post_content, 'sa-sections' ) ) { return; } $css = ''; $design = get_post_meta( $post->ID, self::DESIGN_KEY, true ); if ( is_array( $design ) ) { $css .= "\n" . self::design_css( $design ); } if ( self::is_full( $post->ID ) ) { // Block themes print the title as a block; the hero heading is the page's H1. // Block themes put their top spacing on these wrappers as inline styles, hence !important. $css .= "\nbody.gralus-landing .wp-block-post-title{display:none}" . "\nbody.gralus-landing .wp-site-blocks>main,body.gralus-landing main.wp-block-group{margin-top:0!important;padding-top:0!important}" . "\nbody.gralus-landing main .wp-block-group:has(>.wp-block-post-content){padding-top:0!important}" . "\nbody.gralus-landing .wp-block-post-content{margin-block-start:0!important}"; } wp_enqueue_style( 'gralus-sections', plugins_url( 'assets/sections.css', GRALUS_FILE ), array(), GRALUS_VERSION ); if ( '' !== $css ) { wp_add_inline_style( 'gralus-sections', $css ); } } /** Store the page's design values and layout. Values are checked here; anything malformed is dropped. */ public static function save_look( $post_id, $design, $layout ) { $clean = self::clean_design( $design ); if ( $clean ) { update_post_meta( $post_id, self::DESIGN_KEY, $clean ); } elseif ( null !== $design ) { delete_post_meta( $post_id, self::DESIGN_KEY ); } if ( 'full' === $layout ) { update_post_meta( $post_id, self::LAYOUT_KEY, 'full' ); } elseif ( null !== $layout ) { delete_post_meta( $post_id, self::LAYOUT_KEY ); } } /** Only colours, px numbers and font names get through. */ public static function clean_design( $d ) { if ( ! is_array( $d ) ) { return array(); } $out = array(); foreach ( array( 'accent', 'onAccent', 'ink', 'background' ) as $k ) { if ( isset( $d[ $k ] ) && is_string( $d[ $k ] ) && preg_match( '/^#[0-9a-fA-F]{6}$/', $d[ $k ] ) ) { $out[ $k ] = strtolower( $d[ $k ] ); } } foreach ( array( 'radius' => 32, 'buttonRadius' => 999 ) as $k => $max ) { if ( isset( $d[ $k ] ) && is_numeric( $d[ $k ] ) ) { $out[ $k ] = max( 0, min( $max, (int) $d[ $k ] ) ); } } if ( isset( $d['width'] ) && is_numeric( $d['width'] ) && (int) $d['width'] >= 900 && (int) $d['width'] <= 1600 ) { $out['width'] = (int) $d['width']; } if ( isset( $d['headerVar'] ) && is_string( $d['headerVar'] ) && preg_match( '/^--[a-zA-Z0-9-]{1,40}$/', $d['headerVar'] ) ) { $out['headerVar'] = $d['headerVar']; } foreach ( array( 'headingFont', 'bodyFont' ) as $k ) { if ( isset( $d[ $k ] ) && is_string( $d[ $k ] ) ) { $f = trim( preg_replace( '/[^\w\s"\',.-]/', '', $d[ $k ] ) ); if ( '' !== $f ) { $out[ $k ] = substr( $f, 0, 200 ); } } } return isset( $out['accent'] ) ? $out : array(); } /** The same CSS Gralus shows in its preview (designCss in packages/core/src/design.ts). */ public static function design_css( $d ) { $vars = array(); $map = array( 'accent' => '--sa-accent', 'onAccent' => '--sa-on-accent', 'ink' => '--sa-ink', 'background' => '--sa-bg' ); foreach ( $map as $k => $var ) { if ( isset( $d[ $k ] ) ) { $vars[] = $var . ':' . $d[ $k ]; } } if ( isset( $d['radius'] ) ) { $vars[] = '--sa-radius:' . (int) $d['radius'] . 'px'; } if ( isset( $d['buttonRadius'] ) ) { $vars[] = '--sa-btn-radius:' . (int) $d['buttonRadius'] . 'px'; } if ( isset( $d['width'] ) ) { $vars[] = '--sa-width:' . (int) $d['width'] . 'px'; } if ( isset( $d['headerVar'] ) ) { $vars[] = '--sa-header-offset:var(' . $d['headerVar'] . ')'; } if ( isset( $d['headingFont'] ) ) { $vars[] = '--sa-heading-font:' . $d['headingFont']; } if ( isset( $d['bodyFont'] ) ) { $vars[] = '--sa-body-font:' . $d['bodyFont']; } $out = array( '.sa-sections{' . implode( ';', $vars ) . '}' ); if ( isset( $d['bodyFont'] ) ) { $out[] = '.sa-sections{font-family:' . $d['bodyFont'] . '}'; } if ( isset( $d['headingFont'] ) ) { $out[] = '.sa-sections h1,.sa-sections h2,.sa-sections h3{font-family:' . $d['headingFont'] . '}'; } return implode( "\n", $out ); } public static function is_full( $post_id ) { return 'full' === get_post_meta( $post_id, self::LAYOUT_KEY, true ); } /** * Full-width landing pages in classic themes: the theme's header and footer around the sections, without the * theme's page title and narrow content column. Block themes keep their own template (the sections are full width there). */ public static function template( $template ) { if ( ! is_singular( 'page' ) || ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) ) { return $template; } $id = get_queried_object_id(); if ( ! $id || ! self::is_full( $id ) || false === strpos( (string) get_post_field( 'post_content', $id ), 'sa-sections' ) ) { return $template; } return plugin_dir_path( GRALUS_FILE ) . 'templates/landing.php'; } public static function body_class( $classes ) { if ( is_singular( 'page' ) && self::is_full( get_queried_object_id() ) ) { $classes[] = 'gralus-landing'; } return $classes; } /** Store FAQ pairs sent with a post (array of {q,a}); an empty list removes them. */ public static function save_faq( $post_id, $faq ) { $clean = array(); if ( is_array( $faq ) ) { foreach ( array_slice( $faq, 0, 20 ) as $item ) { $q = isset( $item['q'] ) ? sanitize_text_field( $item['q'] ) : ''; $a = isset( $item['a'] ) ? sanitize_textarea_field( $item['a'] ) : ''; if ( '' !== $q && '' !== $a ) { $clean[] = array( 'q' => $q, 'a' => $a ); } } } if ( $clean ) { update_post_meta( $post_id, self::FAQ_KEY, $clean ); } else { delete_post_meta( $post_id, self::FAQ_KEY ); } } /** FAQPage schema for AI assistants and search engines. */ public static function print_schema() { if ( ! is_singular() ) { return; } $faq = get_post_meta( get_queried_object_id(), self::FAQ_KEY, true ); if ( empty( $faq ) || ! is_array( $faq ) ) { return; } $entities = array(); foreach ( $faq as $item ) { $entities[] = array( '@type' => 'Question', 'name' => $item['q'], 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $item['a'] ), ); } $schema = array( '@context' => 'https://schema.org', '@type' => 'FAQPage', 'mainEntity' => $entities ); // JSON_HEX_TAG: a "" inside an answer can't close the tag. wp_print_inline_script_tag( wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG ), array( 'type' => 'application/ld+json' ) ); } }