This commit is contained in:
2026-04-29 19:04:20 +08:00
parent df60e8dd42
commit 07d11a15af
58 changed files with 3937 additions and 520 deletions
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* Register block patterns for CyyWordpress.
*
* @package CyyWordpress
*/
if ( ! function_exists( 'cyywordpress_register_block_patterns' ) ) :
/**
* Register all theme block patterns.
*/
function cyywordpress_register_block_patterns(): void {
// Register pattern category.
register_block_pattern_category(
'cyywordpress',
array( 'label' => esc_html__( 'CyyWordpress', 'cyywordpress' ) )
);
// Load pattern files.
$pattern_files = array(
'hero-banner',
'post-card-grid',
'cta-section',
);
foreach ( $pattern_files as $pattern ) {
$file = get_template_directory() . "/patterns/{$pattern}.php";
if ( file_exists( $file ) ) {
require $file;
}
}
}
endif;
add_action( 'init', 'cyywordpress_register_block_patterns' );
+89 -48
View File
@@ -1,61 +1,102 @@
<?php
/**
* Bulma-Navwalker
* Bulma Navwalker secure, PHP 8.1 compatible rewrite
*
* @package Bulma-Navwalker
* @package CyyWordpress
*/
/**
* Class Name: Navwalker
* Plugin Name: Bulma Navwalker
* Plugin URI: https://github.com/Poruno/Bulma-Navwalker
* Description: An extended Wordpress Navwalker object that displays Bulma framework's Navbar https://bulma.io/ in Wordpress.
* Author: Carlo Operio - https://www.linkedin.com/in/carlooperio/, Bulma-Framework
* Author URI: https://github.com/wp-bootstrap
* License: GPL-3.0+
* License URI: https://github.com/Poruno/Bulma-Navwalker/blob/master/LICENSE
*/
if ( ! class_exists( 'Bulmapress_Navwalker' ) ) :
class bulmapress_navwalker extends Walker_Nav_Menu {
/**
* Custom Bulma nav-walker for WordPress menus.
*/
class Bulmapress_Navwalker extends Walker_Nav_Menu {
public function start_lvl( &$output, $depth = 0, $args = array() ) {
$output .= "<div class='navbar-dropdown'>";
}
public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
$liClasses = 'navbar-item '.$item->title;
$hasChildren = $args->walker->has_children;
$liClasses .= $hasChildren? " has-dropdown is-hoverable": "";
if($hasChildren){
$output .= "<div class='".$liClasses."'>";
$output .= "\n<a class='navbar-link' href='".$item->url."'>".$item->title."</a>";
}
else {
$output .= "<a class='".$liClasses."' href='".$item->url."'>".$item->title;
/**
* Opens a dropdown container.
*
* @param string $output Used to append additional content (passed by reference).
* @param int $depth Depth of menu item.
* @param stdClass|null $args An object of wp_nav_menu() arguments.
*/
public function start_lvl( string &$output, int $depth = 0, ?stdClass $args = null ): void {
$output .= '<div class="navbar-dropdown">';
}
// Adds has_children class to the item so end_el can determine if the current element has children
if ( $hasChildren ) {
$item->classes[] = 'has_children';
/**
* Opens a menu item element and its link.
*
* @param string $output Used to append additional content (passed by reference).
* @param WP_Post $item Menu item data object.
* @param int $depth Depth of menu item.
* @param stdClass|null $args An object of wp_nav_menu() arguments.
* @param int $id Current item ID.
*/
public function start_el( string &$output, \WP_Post $item, int $depth = 0, ?stdClass $args = null, int $id = 0 ): void {
$has_children = isset( $args->walker ) && $args->walker->has_children;
if ( $has_children ) {
// Mark the item so end_el knows it needs a </div> instead of </a>.
$item->classes[] = 'has_children';
$output .= '<div class="navbar-item has-dropdown is-hoverable">';
$output .= '<a class="navbar-link" href="' . esc_url( $item->url ) . '">'
. esc_html( $item->title )
. '</a>';
} else {
$output .= '<a class="navbar-item" href="' . esc_url( $item->url ) . '">'
. esc_html( $item->title );
}
}
/**
* Closes a menu item element.
*
* @param string $output Used to append additional content (passed by reference).
* @param WP_Post $item Menu item data object.
* @param int $depth Depth of menu item.
* @param stdClass|null $args An object of wp_nav_menu() arguments.
*/
public function end_el( string &$output, \WP_Post $item, int $depth = 0, ?stdClass $args = null ): void {
if ( in_array( 'has_children', (array) $item->classes, true ) ) {
// Dropdown wrapper opened with <div>; close the div (inner <a> was already closed).
$output .= '</div>';
} else {
// Regular item opened with <a>; close it.
$output .= '</a>';
}
}
/**
* Closes a dropdown container.
*
* @param string $output Used to append additional content (passed by reference).
* @param int $depth Depth of menu item.
* @param stdClass|null $args An object of wp_nav_menu() arguments.
*/
public function end_lvl( string &$output, int $depth = 0, ?stdClass $args = null ): void {
$output .= '</div>'; // .navbar-dropdown
}
/**
* Fallback: output a link to the home page when no menu is assigned.
*
* @param array $args Arguments from wp_nav_menu().
*/
public static function fallback( array $args ): void {
if ( current_user_can( 'manage_options' ) ) {
echo '<a class="navbar-item" href="' . esc_url( admin_url( 'nav-menus.php' ) ) . '">'
. esc_html__( '请添加菜单', 'cyywordpress' )
. '</a>';
} else {
echo '<a class="navbar-item" href="' . esc_url( home_url( '/' ) ) . '">'
. esc_html__( '首页', 'cyywordpress' )
. '</a>';
}
}
}
public function end_el(&$output, $item, $depth = 0, $args = array(), $id = 0 ){
if(in_array("has_children", $item->classes)) {
// Back-compat alias so existing wp_nav_menu() calls still work.
class_alias( 'Bulmapress_Navwalker', 'bulmapress_navwalker' );
$output .= "</div>";
}
$output .= "</a>";
}
public function end_lvl (&$output, $depth = 0, $args = array()) {
$output .= "</div>";
}
}
?>
endif;
+150
View File
@@ -0,0 +1,150 @@
<?php
/**
* Asset registration and enqueueing class.
*
* Reads the Vite manifest (assets/.vite/manifest.json) in production
* and falls back to direct paths in development mode.
*
* @package CyyWordpress
*/
if ( ! class_exists( 'CyyWordpress_Assets' ) ) :
/**
* Handles all script / style enqueueing for the theme.
*/
class CyyWordpress_Assets {
/** Absolute path to the compiled assets directory. */
private const ASSETS_DIR = __DIR__ . '/../assets';
/** URL to the compiled assets directory. */
private static string $assets_url = '';
/** Parsed Vite manifest. */
private static array $manifest = [];
/**
* Registers hooks.
*/
public static function init(): void {
self::$assets_url = get_template_directory_uri() . '/assets';
self::load_manifest();
add_action( 'wp_enqueue_scripts', array( static::class, 'enqueue_frontend' ) );
add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_admin' ) );
add_filter( 'script_loader_tag', array( static::class, 'add_defer_attr' ), 10, 2 );
}
/**
* Loads and caches the Vite manifest if it exists.
*/
private static function load_manifest(): void {
$manifest_path = self::ASSETS_DIR . '/.vite/manifest.json';
if ( file_exists( $manifest_path ) ) {
$json = file_get_contents( $manifest_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ( $json ) {
self::$manifest = json_decode( $json, true ) ?? [];
}
}
}
/**
* Returns the hashed asset URL from the Vite manifest
* or falls back to the raw path.
*
* @param string $entry The entry key (e.g. "src/ts/main.ts").
* @param string $type 'js' or 'css'.
*/
private static function asset_url( string $entry, string $type = 'js' ): string {
if ( ! empty( self::$manifest[ $entry ][ 'file' ] ) ) {
return self::$assets_url . '/' . self::$manifest[ $entry ][ 'file' ];
}
// Dev fallback.
return self::$assets_url . "/$type/" . basename( $entry, '.ts' ) . '.' . $type;
}
/**
* Returns the filemtime-based version string for a local file,
* used for cache-busting in development.
*
* @param string $path Relative path from assets dir.
*/
private static function file_version( string $path ): string {
$abs = self::ASSETS_DIR . '/' . ltrim( $path, '/' );
return file_exists( $abs ) ? (string) filemtime( $abs ) : _S_VERSION;
}
/**
* Enqueue front-end styles and scripts.
*/
public static function enqueue_frontend(): void {
// Main CSS.
$css_url = self::asset_url( 'src/ts/main.ts', 'css' );
wp_enqueue_style(
'cyywordpress-main',
$css_url,
array(),
_S_VERSION
);
wp_style_add_data( 'cyywordpress-main', 'rtl', 'replace' );
// Legacy style.css (theme header required by WP).
wp_enqueue_style(
'cyywordpress-style',
get_stylesheet_uri(),
array( 'cyywordpress-main' ),
_S_VERSION
);
// Main JS.
$js_url = self::asset_url( 'src/ts/main.ts', 'js' );
wp_enqueue_script(
'cyywordpress-main',
$js_url,
array(),
_S_VERSION,
true
);
// Font Awesome (local).
wp_enqueue_style(
'font-awesome',
get_template_directory_uri() . '/node_modules/@fortawesome/fontawesome-free/css/all.min.css',
array(),
'6.5.2'
);
// Comment reply: only when needed.
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
wp_enqueue_script( 'comment-reply' );
}
}
/**
* Enqueue admin / Customizer scripts.
*/
public static function enqueue_admin(): void {
// Customizer preview script is handled via customize_preview_init hook.
}
/**
* Add `defer` attribute to all theme JS files.
*
* @param string $tag The <script> HTML tag.
* @param string $handle The registered script handle.
*/
public static function add_defer_attr( string $tag, string $handle ): string {
$defer_handles = array( 'cyywordpress-main' );
if ( in_array( $handle, $defer_handles, true ) ) {
return str_replace( ' src=', ' defer src=', $tag );
}
return $tag;
}
}
endif;
+154
View File
@@ -0,0 +1,154 @@
<?php
/**
* JSON-LD Schema.org structured data class.
*
* Outputs inline <script type="application/ld+json"> on relevant pages.
* Skips output when a dedicated SEO plugin is active.
*
* @package CyyWordpress
*/
if ( ! class_exists( 'CyyWordpress_Schema' ) ) :
/**
* Handles JSON-LD output.
*/
class CyyWordpress_Schema {
/**
* Register hooks.
*/
public static function init(): void {
// Bail early if a popular SEO plugin already handles schema.
if (
defined( 'WPSEO_VERSION' )
|| defined( 'RANKMATH_VERSION' )
|| class_exists( 'All_in_One_SEO_Pack' )
) {
return;
}
add_action( 'wp_head', array( static::class, 'output_schema' ) );
}
/**
* Determine page type and output the correct schema.
*/
public static function output_schema(): void {
$schema = null;
if ( is_front_page() || is_home() ) {
$schema = self::website_schema();
} elseif ( is_singular( 'post' ) ) {
$schema = self::article_schema();
} elseif ( is_author() ) {
$schema = self::person_schema();
}
if ( $schema ) {
// json_encode with JSON_UNESCAPED_UNICODE for readable Chinese output.
echo '<script type="application/ld+json">'
. wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT )
. '</script>' . PHP_EOL;
}
}
/**
* WebSite schema with SearchAction for Sitelink Search Box.
*
* @return array<string, mixed>
*/
private static function website_schema(): array {
return array(
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
'potentialAction' => array(
'@type' => 'SearchAction',
'target' => array(
'@type' => 'EntryPoint',
'urlTemplate' => home_url( '/?s={search_term_string}' ),
),
'query-input' => 'required name=search_term_string',
),
);
}
/**
* Article schema for single posts.
*
* @return array<string, mixed>
*/
private static function article_schema(): array {
global $post;
$author_id = (int) get_the_author_meta( 'ID' );
$author_name = get_the_author_meta( 'display_name', $author_id );
$author_url = get_author_posts_url( $author_id );
$image_url = '';
if ( has_post_thumbnail() ) {
$src = wp_get_attachment_image_src( (int) get_post_thumbnail_id(), 'large' );
if ( $src ) {
$image_url = $src[0];
}
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => get_the_title(),
'url' => (string) get_permalink(),
'datePublished' => get_the_date( DATE_W3C ),
'dateModified' => get_the_modified_date( DATE_W3C ),
'author' => array(
'@type' => 'Person',
'name' => $author_name,
'url' => $author_url,
),
'publisher' => array(
'@type' => 'Organization',
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
),
'description' => has_excerpt()
? get_the_excerpt()
: wp_trim_words( wp_strip_all_tags( $post->post_content ), 30 ),
);
if ( $image_url ) {
$schema['image'] = $image_url;
}
return $schema;
}
/**
* Person schema for author archive pages.
*
* @return array<string, mixed>
*/
private static function person_schema(): array {
$author = get_queried_object();
$author_id = ( $author instanceof WP_User ) ? $author->ID : 0;
$author_name = get_the_author_meta( 'display_name', $author_id );
$author_url = get_author_posts_url( $author_id );
$bio = get_the_author_meta( 'description', $author_id );
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Person',
'name' => $author_name,
'url' => $author_url,
);
if ( $bio ) {
$schema['description'] = $bio;
}
return $schema;
}
}
endif;
+124
View File
@@ -0,0 +1,124 @@
<?php
/**
* SEO / Meta-tags class.
*
* Outputs Open Graph and Twitter Card meta tags.
* Does NOT run if a dedicated SEO plugin (Yoast, RankMath, AIOSEO) is active.
*
* @package CyyWordpress
*/
if ( ! class_exists( 'CyyWordpress_SEO' ) ) :
/**
* Manages Open Graph + Twitter Card meta output.
*/
class CyyWordpress_SEO {
/**
* Register hooks.
*/
public static function init(): void {
// Bail early if a popular SEO plugin is active.
if (
defined( 'WPSEO_VERSION' ) // Yoast SEO
|| defined( 'RANKMATH_VERSION' ) // RankMath
|| class_exists( 'All_in_One_SEO_Pack' )
) {
return;
}
add_action( 'wp_head', array( static::class, 'output_meta_tags' ), 5 );
}
/**
* Output all meta tags.
*/
public static function output_meta_tags(): void {
$description = self::get_description();
$image = self::get_image();
$type = is_singular() ? 'article' : 'website';
$url = esc_url( self::get_canonical_url() );
$title = esc_attr( wp_get_document_title() );
echo "\n<!-- CyyWordpress SEO Meta -->\n";
// Open Graph.
printf( '<meta property="og:title" content="%s">' . PHP_EOL, esc_attr( $title ) );
printf( '<meta property="og:description" content="%s">' . PHP_EOL, esc_attr( $description ) );
printf( '<meta property="og:url" content="%s">' . PHP_EOL, $url );
printf( '<meta property="og:type" content="%s">' . PHP_EOL, esc_attr( $type ) );
printf( '<meta property="og:site_name" content="%s">' . PHP_EOL, esc_attr( get_bloginfo( 'name' ) ) );
if ( $image ) {
printf( '<meta property="og:image" content="%s">' . PHP_EOL, esc_url( $image ) );
printf( '<meta property="og:image:alt" content="%s">' . PHP_EOL, esc_attr( $title ) );
}
// Twitter Card.
$card_type = $image ? 'summary_large_image' : 'summary';
printf( '<meta name="twitter:card" content="%s">' . PHP_EOL, esc_attr( $card_type ) );
printf( '<meta name="twitter:title" content="%s">' . PHP_EOL, esc_attr( $title ) );
printf( '<meta name="twitter:description" content="%s">' . PHP_EOL, esc_attr( $description ) );
if ( $image ) {
printf( '<meta name="twitter:image" content="%s">' . PHP_EOL, esc_url( $image ) );
}
echo "<!-- /CyyWordpress SEO Meta -->\n\n";
}
/**
* Returns a cleaned excerpt / description.
*/
private static function get_description(): string {
if ( is_singular() ) {
global $post;
$excerpt = has_excerpt() ? get_the_excerpt() : wp_trim_words( wp_strip_all_tags( $post->post_content ), 30 );
return sanitize_text_field( $excerpt );
}
$tagline = get_bloginfo( 'description' );
return sanitize_text_field( $tagline );
}
/**
* Returns the OG image URL.
*/
private static function get_image(): string {
if ( is_singular() && has_post_thumbnail() ) {
$src = wp_get_attachment_image_src( (int) get_post_thumbnail_id(), 'large' );
if ( $src ) {
return $src[0];
}
}
// Site-wide fallback: custom logo.
$logo_id = get_theme_mod( 'custom_logo' );
if ( $logo_id ) {
$src = wp_get_attachment_image_src( (int) $logo_id, 'full' );
if ( $src ) {
return $src[0];
}
}
return '';
}
/**
* Returns the canonical URL for the current page.
*/
private static function get_canonical_url(): string {
if ( is_singular() ) {
return (string) get_permalink();
}
if ( is_home() ) {
return home_url( '/' );
}
return home_url( add_query_arg( array() ) );
}
}
endif;
+63 -12
View File
@@ -6,13 +6,13 @@
*/
/**
* Add postMessage support for site title and description for the Theme Customizer.
* Add postMessage support and custom options for the Theme Customizer.
*
* @param WP_Customize_Manager $wp_customize Theme Customizer object.
*/
function cyywordpress_customize_register( $wp_customize ) {
$wp_customize->get_setting( 'blogname' )->transport = 'postMessage';
$wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage';
function cyywordpress_customize_register( WP_Customize_Manager $wp_customize ): void {
$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 ) ) {
@@ -31,31 +31,82 @@ function cyywordpress_customize_register( $wp_customize ) {
)
);
}
// -----------------------------------------------------------------------
// Section: Front Page Banner
// -----------------------------------------------------------------------
$wp_customize->add_section(
'cyywordpress_frontpage',
array(
'title' => esc_html__( '首页横幅', 'cyywordpress' ),
'priority' => 130,
)
);
$wp_customize->add_setting(
'cyywordpress_welcome_text',
array(
// 存原始字符串,输出时再转义。
'default' => __( '欢迎来到这!Hello everyone.', 'cyywordpress' ),
'sanitize_callback' => 'sanitize_text_field',
'transport' => 'postMessage',
)
);
$wp_customize->add_control(
'cyywordpress_welcome_text',
array(
'label' => esc_html__( '欢迎横幅文字', 'cyywordpress' ),
'section' => 'cyywordpress_frontpage',
'type' => 'text',
)
);
$wp_customize->add_setting(
'cyywordpress_show_welcome_banner',
array(
'default' => true,
'sanitize_callback' => 'rest_sanitize_boolean',
'transport' => 'refresh',
)
);
$wp_customize->add_control(
'cyywordpress_show_welcome_banner',
array(
'label' => esc_html__( '显示欢迎横幅', 'cyywordpress' ),
'section' => 'cyywordpress_frontpage',
'type' => 'checkbox',
)
);
}
add_action( 'customize_register', 'cyywordpress_customize_register' );
/**
* Render the site title for the selective refresh partial.
*
* @return void
*/
function cyywordpress_customize_partial_blogname() {
function cyywordpress_customize_partial_blogname(): void {
bloginfo( 'name' );
}
/**
* Render the site tagline for the selective refresh partial.
*
* @return void
*/
function cyywordpress_customize_partial_blogdescription() {
function cyywordpress_customize_partial_blogdescription(): void {
bloginfo( 'description' );
}
/**
* Binds JS handlers to make Theme Customizer preview reload changes asynchronously.
*/
function cyywordpress_customize_preview_js() {
wp_enqueue_script( 'cyywordpress-customizer', get_template_directory_uri() . '/js/customizer.js', array( 'customize-preview' ), _S_VERSION, true );
function cyywordpress_customize_preview_js(): void {
wp_enqueue_script(
'cyywordpress-customizer',
get_template_directory_uri() . '/js/customizer.js',
array( 'customize-preview' ),
_S_VERSION,
true
);
}
add_action( 'customize_preview_init', 'cyywordpress_customize_preview_js' );