Compare commits

1 Commits

Author SHA1 Message Date
Chuyaoyuan 07d11a15af update 2026-04-29 19:04:20 +08:00
58 changed files with 3937 additions and 520 deletions
+16
View File
@@ -0,0 +1,16 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.{json,yml,yaml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+20
View File
@@ -0,0 +1,20 @@
{
"root": true,
"extends": [
"plugin:@wordpress/eslint-plugin/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-function-return-type": "warn",
"no-console": "warn"
},
"ignorePatterns": ["vendor/", "node_modules/", "assets/", "dist/"]
}
+22
View File
@@ -102,3 +102,25 @@ dist
# TernJS port file # TernJS port file
.tern-port .tern-port
# -----------------------------------------------
# CyyWordpress additions
# -----------------------------------------------
# Build output
assets/js/
assets/css/
assets/.vite/
*.map
# Packaged theme zip
*.zip
# OS / editor
.DS_Store
Thumbs.db
.idea/
.vscode/settings.json
# Composer: vendor is committed for deployment, but generated files are not
composer.lock
+1
View File
@@ -0,0 +1 @@
20
+10 -13
View File
@@ -1,16 +1,13 @@
{ {
"extends": [ "extends": ["stylelint-config-standard-scss"],
"stylelint-config-wordpress/scss"
],
"ignoreFiles": [
"sass/_normalize.scss"
],
"rules": { "rules": {
"font-family-no-missing-generic-family-keyword": null, "color-named": "never",
"no-descending-specificity": null, "declaration-no-important": true,
"block-no-empty": null, "max-nesting-depth": 3,
"no-duplicate-selectors": null, "selector-class-pattern": "^[a-z][a-z0-9-_]*(__[a-z0-9-_]+)?(--[a-z0-9-_]+)?$",
"font-family-no-duplicate-names": null, "scss/dollar-variable-pattern": "^[a-z][a-z0-9-_]*$",
"selector-class-pattern": null "scss/at-mixin-pattern": "^[a-z][a-z0-9-_]*$"
} },
"ignoreFiles": ["node_modules/**", "vendor/**", "assets/**"]
} }
+124 -1
View File
@@ -1,2 +1,125 @@
# CyyWordpress # CyyWordpress
一个自用的Wordpress主题
一个基于 **Bulma 1.x** + **Underscores (_s)** 的现代 WordPress 主题,专为中文博客场景设计。
---
## 环境要求
| 依赖 | 最低版本 |
|------|---------|
| PHP | 8.1 |
| WordPress | 6.0 |
| Node.js | 20(见 `.nvmrc`|
| npm | 10+ |
---
## 本地开发环境配置
```bash
# 1. 安装正确的 Node.js 版本(推荐使用 nvm
nvm use # 读取 .nvmrc 自动切换到 Node 20
# 2. 安装前端依赖
npm install
# 3. 启动 Vite 开发服务器(HMR)
npm run dev
# 4. 生产构建(含 contenthash 文件指纹)
npm run build
# 5. 生成 RTL 样式
npm run build:rtl
```
> **注意:** 开发时,将 WordPress 的 `WP_HOME` 指向 `http://localhost:3000`
> Vite 会代理所有请求到本地 WordPress 实例并启用热更新。
---
## 构建流程说明
| 命令 | 说明 |
|------|------|
| `npm run dev` | 启动 Vite 开发服务器(端口 3000,支持 HMR)|
| `npm run build` | 生产构建,输出到 `assets/`(含 contenthash|
| `npm run build:rtl` | 生成 `assets/css/main-rtl.css` |
| `npm run lint` | 运行 ESLint + StyleLint |
| `npm run type-check` | TypeScript 类型检查(不生成文件)|
构建产物由 `inc/class-cyywordpress-assets.php` 读取 `assets/.vite/manifest.json`
自动解析带 hash 的文件名注入到 `wp_enqueue_*`
---
## SCSS 变量覆盖(子主题)
在子主题的入口 SCSS 文件中,在 `@use 'cyywordpress/scss/abstracts/variables'` **之前**
重新声明你希望覆盖的 CSS 自定义属性:
```scss
:root {
--color-primary: #6366f1; /* 换成紫色主题 */
--color-primary-dark: #4f46e5;
}
```
所有组件均使用 `var(--color-primary)` 引用,覆盖后全局生效,无需修改源文件。
---
## Customizer 可配置项
后台路径:**外观 → 自定义 → 首页横幅**
| 选项 | 说明 | 默认值 |
|------|------|--------|
| 欢迎横幅文字 | 首页/文章列表顶部的通知文字 | `欢迎来到这!Hello everyone.` |
| 显示欢迎横幅 | 是否在首页显示横幅 | 勾选 |
---
## 导航菜单位置
| 位置标识 | 显示位置 |
|---------|---------|
| `menu-1` | 顶部主导航 |
| `menu-2` | 页脚导航 |
| `social` | 页脚社交媒体图标链接 |
---
## Widget 区域
| 区域 ID | 位置 |
|---------|------|
| `sidebar-1` | 文章/页面侧边栏 |
| `sidebar-footer-1` | 页脚左列 |
| `sidebar-footer-2` | 页脚中列 |
| `sidebar-footer-3` | 页脚右列 |
| `sidebar-header` | 导航栏右侧 |
---
## 块模式 (Block Patterns)
注册在 `patterns/` 目录,可在编辑器 **插入块 → 模式 → CyyWordpress** 中使用:
- **Hero 横幅** — 全宽渐变横幅 + CTA 按钮
- **文章卡片网格** — 三列响应式卡片布局
- **行动号召区域** — 居中标题 + 双按钮
---
## PHP 代码规范检查
```bash
composer install
./vendor/bin/phpcs
./vendor/bin/phpcbf # 自动修复
```
规范基于 WordPress Coding Standards + PHPCompatibilityPHP 8.1+)。
+5 -4
View File
@@ -15,13 +15,14 @@
} }
], ],
"require": { "require": {
"php": ">=5.6" "php": ">=8.1"
}, },
"require-dev": { "require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
"wptrt/wpthemereview": "^0.2.1", "wptrt/wpthemereview": "^0.2.1",
"php-parallel-lint/php-parallel-lint": "^1.2.0", "php-parallel-lint/php-parallel-lint": "^1.3.0",
"wp-cli/i18n-command": "^2.2.5" "wp-cli/i18n-command": "^2.6.0",
"phpcompatibility/phpcompatibility-wp": "^2.1"
}, },
"scripts": { "scripts": {
"lint:wpcs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", "lint:wpcs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs",
+95 -62
View File
@@ -2,7 +2,8 @@
/** /**
* The template for displaying the footer * The template for displaying the footer
* *
* Contains the closing of the #content div and all content after. * Three-column layout: site info | footer nav (menu-2) | social links (social).
* Dynamic copyright year. All hardcoded external links removed.
* *
* @link https://developer.wordpress.org/themes/basics/template-files/#template-partials * @link https://developer.wordpress.org/themes/basics/template-files/#template-partials
* *
@@ -10,75 +11,107 @@
*/ */
?> ?>
</div><!-- #content .site-content .container -->
</div> </div><!-- #page .site -->
<footer id="colophon" class="site-footer footer">
<div class="container">
<div class="columns">
<footer class="footer"> <!-- Column 1: Site info + footer widget 1 -->
<div class="content has-text-centered"> <div class="column is-4">
<p> <p class="footer-site-name">
<a href="<?php echo esc_url( __( 'https://wordpress.org/', 'cyywordpress' ) ); ?>"> <a href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<?php <?php bloginfo( 'name' ); ?>
/* translators: %s: CMS name, i.e. WordPress. */
printf( esc_html__( '自豪地使用 %s', 'cyywordpress' ), 'WordPress' );
?>
</a> </a>
</p>
<?php
$description = get_bloginfo( 'description' );
if ( $description ) :
echo '<p class="footer-site-description">' . esc_html( $description ) . '</p>';
endif;
?>
<?php if ( is_active_sidebar( 'sidebar-footer-1' ) ) : ?>
<div class="footer-widget-area">
<?php dynamic_sidebar( 'sidebar-footer-1' ); ?>
</div>
<?php endif; ?>
</div><!-- .column -->
<span class="sep"> | </span> <!-- Column 2: Footer navigation (menu-2) + widget 2 -->
Theme: <a href="http://chuyaoyuan.com/">cyywordpress</a> by Chuyaoyuan(自用主题). <div class="column is-4">
<?php if ( has_nav_menu( 'menu-2' ) ) : ?>
<nav class="footer-nav" aria-label="<?php esc_attr_e( '页脚导航', 'cyywordpress' ); ?>">
<?php
wp_nav_menu(
array(
'theme_location' => 'menu-2',
'menu_id' => 'footer-menu',
'depth' => 1,
'container' => false,
'menu_class' => 'footer-menu-list',
)
);
?>
</nav>
<?php endif; ?>
<?php if ( is_active_sidebar( 'sidebar-footer-2' ) ) : ?>
<div class="footer-widget-area">
<?php dynamic_sidebar( 'sidebar-footer-2' ); ?>
</div>
<?php endif; ?>
</div><!-- .column -->
<!-- Column 3: Social links (social menu) + widget 3 -->
<div class="column is-4">
<?php if ( has_nav_menu( 'social' ) ) : ?>
<nav class="social-nav" aria-label="<?php esc_attr_e( '社交媒体链接', 'cyywordpress' ); ?>">
<?php
wp_nav_menu(
array(
'theme_location' => 'social',
'menu_id' => 'social-menu',
'depth' => 1,
'container' => false,
'menu_class' => 'social-menu-list',
'link_before' => '<span class="screen-reader-text">',
'link_after' => '</span>',
)
);
?>
</nav>
<?php endif; ?>
<?php if ( is_active_sidebar( 'sidebar-footer-3' ) ) : ?>
<div class="footer-widget-area">
<?php dynamic_sidebar( 'sidebar-footer-3' ); ?>
</div>
<?php endif; ?>
</div><!-- .column -->
</div><!-- .columns -->
</div><!-- .container -->
<div class="footer-bottom">
<div class="container has-text-centered">
<p class="footer-copyright">
&copy; <?php echo esc_html( (string) gmdate( 'Y' ) ); ?>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
<?php bloginfo( 'name' ); ?>
</a>.
<?php
printf(
/* translators: %s: WordPress link */
esc_html__( '基于 %s 强力驱动', 'cyywordpress' ),
'<a href="https://wordpress.org/" target="_blank" rel="noopener">WordPress</a>'
);
?>
</p> </p>
</div> </div>
</footer> </div>
</footer><!-- #colophon -->
</div><!-- #page -->
<?php wp_footer(); ?> <?php wp_footer(); ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Check if there are any navbar burgers
if ($navbarBurgers.length > 0) {
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
}
});
// 当网页向下滑动 20px 出现"返回顶部" 按钮
window.onscroll = function() {scrollFunction()};
function scrollFunction() {console.log(121);
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
document.getElementById("myBtn").style.display = "block";
} else {
document.getElementById("myBtn").style.display = "none";
}
}
// 点击按钮,返回顶部
function topFunction() {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
</script>
</body> </body>
</html> </html>
+170 -123
View File
@@ -7,57 +7,81 @@
* @package CyyWordpress * @package CyyWordpress
*/ */
if ( ! defined( '_S_VERSION' ) ) { // ---------------------------------------------------------------------------
// Replace the version number of the theme on each release. // PHP & WordPress version guards
define( '_S_VERSION', '1.0.0' ); // ---------------------------------------------------------------------------
if ( version_compare( PHP_VERSION, '8.1', '<' ) ) {
add_action(
'admin_notices',
function (): void {
echo '<div class="notice notice-error"><p>'
. esc_html__( 'CyyWordpress 主题需要 PHP 8.1 或更高版本。', 'cyywordpress' )
. '</p></div>';
}
);
return; // Do not load theme in unsupported environments.
} }
global $wp_version;
if ( version_compare( $wp_version, '6.0', '<' ) ) {
add_action(
'admin_notices',
function (): void {
echo '<div class="notice notice-error"><p>'
. esc_html__( 'CyyWordpress 主题需要 WordPress 6.0 或更高版本。', 'cyywordpress' )
. '</p></div>';
}
);
return;
}
// ---------------------------------------------------------------------------
// Version constant
// ---------------------------------------------------------------------------
if ( ! defined( '_S_VERSION' ) ) {
define( '_S_VERSION', '2.0.0' );
}
// ---------------------------------------------------------------------------
// Includes each file owns its own domain
// ---------------------------------------------------------------------------
require get_template_directory() . '/inc/custom-header.php';
require get_template_directory() . '/inc/template-tags.php';
require get_template_directory() . '/inc/template-functions.php';
require get_template_directory() . '/inc/customizer.php';
require get_template_directory() . '/inc/bulmapress_navwalker.php';
require get_template_directory() . '/inc/search.php';
require get_template_directory() . '/inc/pagination.php';
require get_template_directory() . '/inc/block-patterns.php';
require get_template_directory() . '/inc/class-cyywordpress-assets.php';
require get_template_directory() . '/inc/class-cyywordpress-seo.php';
require get_template_directory() . '/inc/class-cyywordpress-schema.php';
if ( defined( 'JETPACK__VERSION' ) ) {
require get_template_directory() . '/inc/jetpack.php';
}
// ---------------------------------------------------------------------------
// Theme setup
// ---------------------------------------------------------------------------
if ( ! function_exists( 'cyywordpress_setup' ) ) : if ( ! function_exists( 'cyywordpress_setup' ) ) :
/** /**
* Sets up theme defaults and registers support for various WordPress features. * Sets up theme defaults and registers WordPress feature support.
*
* Note that this function is hooked into the after_setup_theme hook, which
* runs before the init hook. The init hook is too late for some features, such
* as indicating support for post thumbnails.
*/
function cyywordpress_setup() {
/*
* Make theme available for translation.
* Translations can be filed in the /languages/ directory.
* If you're building a theme based on CyyWordpress, use a find and replace
* to change 'cyywordpress' to the name of your theme in all the template files.
*/ */
function cyywordpress_setup(): void {
load_theme_textdomain( 'cyywordpress', get_template_directory() . '/languages' ); load_theme_textdomain( 'cyywordpress', get_template_directory() . '/languages' );
// Add default posts and comments RSS feed links to head.
add_theme_support( 'automatic-feed-links' ); add_theme_support( 'automatic-feed-links' );
/*
* Let WordPress manage the document title.
* By adding theme support, we declare that this theme does not use a
* hard-coded <title> tag in the document head, and expect WordPress to
* provide it for us.
*/
add_theme_support( 'title-tag' ); add_theme_support( 'title-tag' );
/*
* Enable support for Post Thumbnails on posts and pages.
*
* @link https://developer.wordpress.org/themes/functionality/featured-images-post-thumbnails/
*/
add_theme_support( 'post-thumbnails' ); add_theme_support( 'post-thumbnails' );
add_theme_support( 'responsive-embeds' );
add_theme_support( 'align-wide' );
add_theme_support( 'widgets-block-editor' );
// This theme uses wp_nav_menu() in one location. // Editor styles keeps back-end visually consistent with the front end.
register_nav_menus( add_theme_support( 'editor-styles' );
array( add_editor_style( 'assets/css/editor-style.css' );
'menu-1' => esc_html__( 'Primary', 'cyywordpress' ),
)
);
/*
* Switch default core markup for search form, comment form, and comments
* to output valid HTML5.
*/
add_theme_support( add_theme_support(
'html5', 'html5',
array( array(
@@ -71,7 +95,6 @@ if ( ! function_exists( 'cyywordpress_setup' ) ) :
) )
); );
// Set up the WordPress core custom background feature.
add_theme_support( add_theme_support(
'custom-background', 'custom-background',
apply_filters( apply_filters(
@@ -83,116 +106,140 @@ if ( ! function_exists( 'cyywordpress_setup' ) ) :
) )
); );
// Add theme support for selective refresh for widgets.
add_theme_support( 'customize-selective-refresh-widgets' ); add_theme_support( 'customize-selective-refresh-widgets' );
/**
* Add support for core custom logo.
*
* @link https://codex.wordpress.org/Theme_Logo
*/
add_theme_support( add_theme_support(
'custom-logo', 'custom-logo',
array( array(
'height' => 250, 'height' => 80,
'width' => 250, 'width' => 80,
'flex-width' => true, 'flex-width' => true,
'flex-height' => true, 'flex-height' => true,
) )
); );
// Navigation menus.
register_nav_menus(
array(
'menu-1' => esc_html__( '主导航', 'cyywordpress' ),
'menu-2' => esc_html__( '页脚导航', 'cyywordpress' ),
'social' => esc_html__( '社交媒体链接', 'cyywordpress' ),
)
);
// Custom image sizes.
add_image_size( 'card-thumbnail', 800, 450, true );
add_image_size( 'card-thumbnail-sm', 400, 225, true );
} }
endif; endif;
add_action( 'after_setup_theme', 'cyywordpress_setup' ); add_action( 'after_setup_theme', 'cyywordpress_setup' );
/** // ---------------------------------------------------------------------------
* Set the content width in pixels, based on the theme's design and stylesheet. // Content width
* // ---------------------------------------------------------------------------
* Priority 0 to make it available to lower priority callbacks. function cyywordpress_content_width(): void {
* $GLOBALS['content_width'] = apply_filters( 'cyywordpress_content_width', 760 );
* @global int $content_width
*/
function cyywordpress_content_width() {
$GLOBALS['content_width'] = apply_filters( 'cyywordpress_content_width', 640 );
} }
add_action( 'after_setup_theme', 'cyywordpress_content_width', 0 ); add_action( 'after_setup_theme', 'cyywordpress_content_width', 0 );
/** // ---------------------------------------------------------------------------
* Register widget area. // Widget areas
* // ---------------------------------------------------------------------------
* @link https://developer.wordpress.org/themes/functionality/sidebars/#registering-a-sidebar function cyywordpress_widgets_init(): void {
*/ register_sidebar(
function cyywordpress_widgets_init() { array(
// register_sidebar( 'name' => esc_html__( '文章侧边栏', 'cyywordpress' ),
// array( 'id' => 'sidebar-1',
// 'name' => esc_html__( 'Sidebar', 'cyywordpress' ), 'description' => esc_html__( '出现在文章/页面侧边的 Widget 区域。', 'cyywordpress' ),
// 'id' => 'sidebar-1', 'before_widget' => '<div id="%1$s" class="widget card %2$s" style="margin-bottom:1rem;">',
// 'description' => esc_html__( 'Add widgets here.', 'cyywordpress' ), 'after_widget' => '</div>',
// 'before_widget' => '<section id="%1$s" class="widget %2$s">', 'before_title' => '<div class="card-header"><p class="card-header-title">',
// 'after_widget' => '</section>', 'after_title' => '</p></div><div class="card-content">',
// 'before_title' => '<h2 class="widget-title">', )
// 'after_title' => '</h2>', );
// )
// );
register_sidebar( register_sidebar(
array( array(
'name' => esc_html__( 'Sidebar', 'cyywordpress' ), 'name' => esc_html__( '页脚 Widget 1', 'cyywordpress' ),
'id' => 'sidebar-1', 'id' => 'sidebar-footer-1',
'description' => esc_html__( 'Add widgets here.', 'cyywordpress' ), 'description' => esc_html__( '页脚第一列 Widget 区域。', 'cyywordpress' ),
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'before_widget' => '<div class="column is-3 %2$s" id="%1$s" >',
'after_widget' => '</div>', 'after_widget' => '</div>',
'before_title' => '<h2 class="widget-title">', 'before_title' => '<h3 class="footer-widget-title">',
'after_title' => '</h2>', 'after_title' => '</h3>',
)
);
register_sidebar(
array(
'name' => esc_html__( '页脚 Widget 2', 'cyywordpress' ),
'id' => 'sidebar-footer-2',
'description' => esc_html__( '页脚第二列 Widget 区域。', 'cyywordpress' ),
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="footer-widget-title">',
'after_title' => '</h3>',
)
);
register_sidebar(
array(
'name' => esc_html__( '页脚 Widget 3', 'cyywordpress' ),
'id' => 'sidebar-footer-3',
'description' => esc_html__( '页脚第三列 Widget 区域。', 'cyywordpress' ),
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="footer-widget-title">',
'after_title' => '</h3>',
)
);
register_sidebar(
array(
'name' => esc_html__( '导航栏右侧', 'cyywordpress' ),
'id' => 'sidebar-header',
'description' => esc_html__( '出现在导航栏右侧的 Widget 区域(搜索等)。', 'cyywordpress' ),
'before_widget' => '<div id="%1$s" class="widget navbar-item %2$s">',
'after_widget' => '</div>',
'before_title' => '<span class="screen-reader-text">',
'after_title' => '</span>',
) )
); );
} }
add_action( 'widgets_init', 'cyywordpress_widgets_init' ); add_action( 'widgets_init', 'cyywordpress_widgets_init' );
/** // ---------------------------------------------------------------------------
* Enqueue scripts and styles. // Initialise asset, SEO and schema classes
*/ // ---------------------------------------------------------------------------
function cyywordpress_scripts() { add_action(
wp_enqueue_style( 'cyywordpress-style', get_stylesheet_uri(), array(), _S_VERSION ); 'after_setup_theme',
wp_style_add_data( 'cyywordpress-style', 'rtl', 'replace' ); function (): void {
CyyWordpress_Assets::init();
wp_enqueue_script( 'cyywordpress-navigation', get_template_directory_uri() . '/js/navigation.js', array(), _S_VERSION, true ); CyyWordpress_SEO::init();
CyyWordpress_Schema::init();
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
wp_enqueue_script( 'comment-reply' );
} }
} );
add_action( 'wp_enqueue_scripts', 'cyywordpress_scripts' );
// ---------------------------------------------------------------------------
// Lazy-load attributes on all attachment images
// ---------------------------------------------------------------------------
add_filter(
'wp_get_attachment_image_attributes',
function ( array $attr ): array {
$attr['loading'] = 'lazy';
$attr['decoding'] = 'async';
return $attr;
}
);
// ---------------------------------------------------------------------------
// Remove WordPress emoji CDN (performance)
// ---------------------------------------------------------------------------
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
/**
* Implement the Custom Header feature.
*/
require get_template_directory() . '/inc/custom-header.php';
/**
* Custom template tags for this theme.
*/
require get_template_directory() . '/inc/template-tags.php';
/**
* Functions which enhance the theme by hooking into WordPress.
*/
require get_template_directory() . '/inc/template-functions.php';
/**
* Customizer additions.
*/
require get_template_directory() . '/inc/customizer.php';
/**
* Load Jetpack compatibility file.
*/
if ( defined( 'JETPACK__VERSION' ) ) {
require get_template_directory() . '/inc/jetpack.php';
}
require get_template_directory() . '/inc/bulmapress_navwalker.php';
require get_template_directory() . '/inc/search.php';
require get_template_directory() . '/inc/pagination.php';
+111 -56
View File
@@ -2,7 +2,8 @@
/** /**
* The header for our theme * The header for our theme
* *
* This is the template that displays all of the <head> section and everything up until <div id="content"> * Displays the <head>, skip-link, navbar with custom logo, primary nav,
* search widget, dark-mode toggle, and back-to-top button.
* *
* @link https://developer.wordpress.org/themes/basics/template-files/#template-partials * @link https://developer.wordpress.org/themes/basics/template-files/#template-partials
* *
@@ -16,83 +17,137 @@
<meta charset="<?php bloginfo( 'charset' ); ?>"> <meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="profile" href="https://gmpg.org/xfn/11"> <link rel="profile" href="https://gmpg.org/xfn/11">
<link crossorigin="anonymous" integrity="sha512-6eqPOYQmUqGh2hFSAKha1dbQlMq1OaxityCVG80dkvGmmLdsAdrAUkgBKDV4fpAT/xOUdkB6uupudSbCwyoJPQ==" href="https://lib.baomitu.com/bulma/0.9.1/css/bulma.css" rel="stylesheet">
<link crossorigin="anonymous" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA==" href="https://lib.baomitu.com/font-awesome/5.15.1/css/all.min.css" rel="stylesheet">
<?php wp_head(); ?> <?php wp_head(); ?>
</head> </head>
<body <?php body_class(); ?>> <body <?php body_class(); ?>>
<?php wp_body_open(); ?> <?php wp_body_open(); ?>
<div id="page" class="site"> <div id="page" class="site">
<!-- <a class="skip-link screen-reader-text" href="#primary">--><?php //esc_html_e( 'Skip to content', 'cyywordpress' ); ?><!--</a>-->
<button onclick="topFunction()" id="myBtn" title="回顶部">返回顶部</button>
<a class="skip-link screen-reader-text" href="#primary">
<?php esc_html_e( '跳到内容', 'cyywordpress' ); ?>
</a>
<nav class="navbar" style="box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06) !important;" role="navigation" aria-label="main navigation"> <!-- Back-to-top button (hidden until scrolled 300px) -->
<button
id="back-to-top"
class="back-to-top"
aria-label="<?php esc_attr_e( '返回顶部', 'cyywordpress' ); ?>"
hidden
>
<i class="fa-solid fa-arrow-up" aria-hidden="true"></i>
</button>
<nav
id="site-navigation"
class="navbar is-spaced site-navbar"
role="navigation"
aria-label="<?php esc_attr_e( '主导航', 'cyywordpress' ); ?>"
>
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="<?php echo esc_url( home_url( '/' ) ); ?>"> <?php if ( has_custom_logo() ) : ?>
<img src="https://s3.ax1x.com/2021/02/26/6SEr4g.jpg" width="28" height="28"> <a class="navbar-item navbar-logo" href="<?php echo esc_url( home_url( '/' ) ); ?>">
<?php the_custom_logo(); ?>
</a> </a>
<p class="subtitle is-4" style=" margin-top: 10px;margin-bottom: 5px;"><a href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home"><?php bloginfo( 'name' ); ?></a></p> <?php else : ?>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"> <a class="navbar-item navbar-site-name" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<span aria-hidden="true"></span> <span class="site-title-initial">
<span aria-hidden="true"></span> <?php echo esc_html( mb_substr( get_bloginfo( 'name' ), 0, 1 ) ); ?>
<span aria-hidden="true"></span> </span>
<span class="site-title-text"><?php bloginfo( 'name' ); ?></span>
</a> </a>
</div> <?php endif; ?>
<div id="navbarBasicExample" class="navbar-menu">
<button
class="navbar-burger"
id="navbar-burger"
aria-label="<?php esc_attr_e( '菜单', 'cyywordpress' ); ?>"
aria-expanded="false"
aria-controls="navbar-menu"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</button>
</div><!-- .navbar-brand -->
<div id="navbar-menu" class="navbar-menu">
<div class="navbar-start"> <div class="navbar-start">
<?php <?php
wp_nav_menu( array( wp_nav_menu(
array(
'theme_location' => 'menu-1', 'theme_location' => 'menu-1',
'menu_id' => 'primary-menu', 'menu_id' => 'primary-menu',
'depth' => 0, 'depth' => 3,
'container' => 'div id="navigation"', 'container' => false,
'menu_class' => 'navbar-end', 'menu_class' => 'navbar-nav-list',
'fallback_cb' => 'bulmapress_navwalker::fallback', 'fallback_cb' => array( 'Bulmapress_Navwalker', 'fallback' ),
'walker' => new bulmapress_navwalker() 'walker' => new Bulmapress_Navwalker(),
) )
); );
?> ?>
<div class="navbar-item has-dropdown is-hoverable"> </div><!-- .navbar-start -->
<a class="navbar-link">
更多
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="https://kaifa.baidu.com/" target="_blank">
开发者搜索(Baidu)
</a>
<a class="navbar-item" href="https://learnku.com/" target="_blank">
编程者论坛(LearnKu)
</a>
<a class="navbar-item" href="http://www.oschina.net/" target="_blank">
程序员资讯(osChina)
</a>
<hr class="navbar-divider">
<a class="navbar-item" href="http://yaoyuan.io" target="_blank">
更多(chuyaoyuan)
</a>
</div>
</div>
</div>
<div class="navbar-end"> <div class="navbar-end">
<!-- Expandable search -->
<div class="navbar-item navbar-search-wrap">
<button
class="navbar-search-toggle"
aria-label="<?php esc_attr_e( '搜索', 'cyywordpress' ); ?>"
aria-expanded="false"
aria-controls="navbar-search-form"
>
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
</button>
<form
id="navbar-search-form"
class="navbar-search-form"
role="search"
method="get"
action="<?php echo esc_url( home_url( '/' ) ); ?>"
hidden
>
<label class="screen-reader-text" for="navbar-search-input">
<?php esc_html_e( '搜索', 'cyywordpress' ); ?>
</label>
<input
id="navbar-search-input"
class="input is-small"
type="search"
name="s"
placeholder="<?php esc_attr_e( '搜索…', 'cyywordpress' ); ?>"
value="<?php echo esc_attr( get_search_query() ); ?>"
>
</form>
</div>
<!-- Dark-mode toggle -->
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <button
<a class="button is-link is-light" href="http://chuyaoyuan.com/wp-admin/"> id="theme-toggle"
<strong>后台登陆</strong> class="theme-toggle"
aria-label="<?php esc_attr_e( '切换深色模式', 'cyywordpress' ); ?>"
>
<i class="fa-solid fa-moon theme-toggle__dark-icon" aria-hidden="true"></i>
<i class="fa-solid fa-sun theme-toggle__light-icon" aria-hidden="true"></i>
</button>
</div>
<?php if ( is_active_sidebar( 'sidebar-header' ) ) : ?>
<?php dynamic_sidebar( 'sidebar-header' ); ?>
<?php endif; ?>
<?php if ( is_user_logged_in() ) : ?>
<div class="navbar-item">
<a class="button is-link is-light is-small" href="<?php echo esc_url( admin_url() ); ?>">
<strong><?php esc_html_e( '后台', 'cyywordpress' ); ?></strong>
</a> </a>
<!-- <a class="button is-light">
退出
</a>-->
</div> </div>
</div> <?php endif; ?>
</div> </div><!-- .navbar-end -->
</div> </div><!-- #navbar-menu -->
</nav> </nav><!-- #site-navigation -->
<div class="container">
<div id="content" class="site-content container">
+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' );
+84 -43
View File
@@ -1,61 +1,102 @@
<?php <?php
/** /**
* Bulma-Navwalker * Bulma Navwalker secure, PHP 8.1 compatible rewrite
* *
* @package Bulma-Navwalker * @package CyyWordpress
*/ */
/** if ( ! class_exists( 'Bulmapress_Navwalker' ) ) :
* Class Name: Navwalker
* Plugin Name: Bulma Navwalker /**
* Plugin URI: https://github.com/Poruno/Bulma-Navwalker * Custom Bulma nav-walker for WordPress menus.
* 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
*/ */
class Bulmapress_Navwalker extends Walker_Nav_Menu {
class bulmapress_navwalker extends Walker_Nav_Menu { /**
* Opens a dropdown container.
public function start_lvl( &$output, $depth = 0, $args = array() ) { *
* @param string $output Used to append additional content (passed by reference).
$output .= "<div class='navbar-dropdown'>"; * @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">';
} }
public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) { /**
* 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;
$liClasses = 'navbar-item '.$item->title; if ( $has_children ) {
// Mark the item so end_el knows it needs a </div> instead of </a>.
$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;
}
// Adds has_children class to the item so end_el can determine if the current element has children
if ( $hasChildren ) {
$item->classes[] = 'has_children'; $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 );
} }
} }
public function end_el(&$output, $item, $depth = 0, $args = array(), $id = 0 ){ /**
* Closes a menu item element.
if(in_array("has_children", $item->classes)) { *
* @param string $output Used to append additional content (passed by reference).
$output .= "</div>"; * @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>';
} }
$output .= "</a>";
} }
public function end_lvl (&$output, $depth = 0, $args = array()) { /**
* Closes a dropdown container.
$output .= "</div>"; *
* @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>';
}
}
}
// Back-compat alias so existing wp_nav_menu() calls still work.
class_alias( 'Bulmapress_Navwalker', 'bulmapress_navwalker' );
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;
+61 -10
View File
@@ -6,11 +6,11 @@
*/ */
/** /**
* 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. * @param WP_Customize_Manager $wp_customize Theme Customizer object.
*/ */
function cyywordpress_customize_register( $wp_customize ) { function cyywordpress_customize_register( WP_Customize_Manager $wp_customize ): void {
$wp_customize->get_setting( 'blogname' )->transport = 'postMessage'; $wp_customize->get_setting( 'blogname' )->transport = 'postMessage';
$wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage'; $wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage';
$wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage'; $wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage';
@@ -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' ); add_action( 'customize_register', 'cyywordpress_customize_register' );
/** /**
* Render the site title for the selective refresh partial. * Render the site title for the selective refresh partial.
*
* @return void
*/ */
function cyywordpress_customize_partial_blogname() { function cyywordpress_customize_partial_blogname(): void {
bloginfo( 'name' ); bloginfo( 'name' );
} }
/** /**
* Render the site tagline for the selective refresh partial. * Render the site tagline for the selective refresh partial.
*
* @return void
*/ */
function cyywordpress_customize_partial_blogdescription() { function cyywordpress_customize_partial_blogdescription(): void {
bloginfo( 'description' ); bloginfo( 'description' );
} }
/** /**
* Binds JS handlers to make Theme Customizer preview reload changes asynchronously. * Binds JS handlers to make Theme Customizer preview reload changes asynchronously.
*/ */
function cyywordpress_customize_preview_js() { function cyywordpress_customize_preview_js(): void {
wp_enqueue_script( 'cyywordpress-customizer', get_template_directory_uri() . '/js/customizer.js', array( 'customize-preview' ), _S_VERSION, true ); 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' ); add_action( 'customize_preview_init', 'cyywordpress_customize_preview_js' );
+64 -46
View File
@@ -2,10 +2,8 @@
/** /**
* The main template file * The main template file
* *
* This is the most generic template file in a WordPress theme * Responsive 1/2/3-column post grid, sticky hero for top-pinned posts,
* and one of the two required files for a theme (the other being style.css). * customiser-driven welcome banner, and pagination.
* It is used to display a page when nothing more specific matches a query.
* E.g., it puts together the home page when no home.php file exists.
* *
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/ * @link https://developer.wordpress.org/themes/basics/template-hierarchy/
* *
@@ -14,69 +12,89 @@
get_header(); get_header();
?> ?>
<section class="section">
<div class="container">
<!-- <h1 class="title">
<?php /*bloginfo( 'name' ); */?>
</h1>-->
<p class="subtitle">
<?php bloginfo( 'description' ); ?>
</p>
</div>
</section>
<br> <div id="primary" class="content-area">
<div class="notification is-primary"> <main id="main" class="site-main" role="main">
欢迎来到这! <strong>Hello everyone.</strong>
</div>
<nav class="breadcrumb" aria-label="breadcrumbs"> <?php
// Welcome banner controlled via Customizer.
$show_banner = get_theme_mod( 'cyywordpress_show_welcome_banner', true );
$welcome_text = get_theme_mod( 'cyywordpress_welcome_text', __( '欢迎来到这!Hello everyone.', 'cyywordpress' ) );
if ( $show_banner && ( is_home() || is_front_page() ) ) :
?>
<div class="notification is-primary welcome-banner" data-customizer="cyywordpress_welcome_text">
<?php echo esc_html( $welcome_text ); ?>
</div>
<?php
endif;
?>
<nav class="breadcrumb" aria-label="<?php esc_attr_e( '面包屑导航', 'cyywordpress' ); ?>">
<ul> <ul>
<li> <li>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" > <a href="<?php echo esc_url( home_url( '/' ) ); ?>">
<span class="icon is-small"> <span class="icon is-small"><i class="fa-solid fa-house" aria-hidden="true"></i></span>
<i class="fas fa-home" aria-hidden="true"></i> <span><?php esc_html_e( '首页', 'cyywordpress' ); ?></span>
</span>
<span>首页</span>
</a> </a>
</li> </li>
<?php if ( is_home() && ! is_front_page() ) : ?>
<li class="is-active">
<a href="#" aria-current="page"><?php single_post_title(); ?></a>
</li>
<?php endif; ?>
</ul> </ul>
</nav> </nav>
<?php if ( have_posts() ) : ?>
<?php <?php
if ( have_posts() ) : // Separate sticky posts for hero treatment.
$sticky_ids = get_option( 'sticky_posts', array() );
if ( is_home() && ! is_front_page() ) : $hero_printed = false;
?> ?>
<p class="title is-4"><?php single_post_title(); ?></p>article
<?php
endif;
/* Start the Loop */ <div class="posts-grid columns is-multiline">
<?php
while ( have_posts() ) : while ( have_posts() ) :
the_post(); the_post();
/* // Sticky → hero card (full-width), only first sticky.
* Include the Post-Type-specific template for the content. if ( is_sticky() && ! $hero_printed && is_home() && ! is_paged() ) :
* If you want to override this in a child theme, then include a file $hero_printed = true;
* called content-___.php (where ___ is the Post Type name) and that will be used instead. echo '<div class="column is-12 post-card--hero">';
*/
get_template_part( 'template-parts/content', get_post_type() ); get_template_part( 'template-parts/content', get_post_type() );
echo '</div>';
else :
echo '<div class="column is-12-mobile is-6-tablet is-4-desktop">';
get_template_part( 'template-parts/content', get_post_type() );
echo '</div>';
endif;
endwhile; endwhile;
the_posts_navigation();
else :
get_template_part( 'template-parts/content', 'none' );
endif;
?> ?>
</div><!-- .posts-grid -->
<div class="posts-pagination">
<?php the_posts_pagination(
array(
'mid_size' => 2,
'prev_text' => '<i class="fa-solid fa-chevron-left" aria-hidden="true"></i> ' . esc_html__( '上一页', 'cyywordpress' ),
'next_text' => esc_html__( '下一页', 'cyywordpress' ) . ' <i class="fa-solid fa-chevron-right" aria-hidden="true"></i>',
)
); ?>
</div>
<?php else : ?>
<?php get_template_part( 'template-parts/content', 'none' ); ?>
<?php endif; ?>
</main><!-- #main -->
</div><!-- #primary -->
<?php
get_sidebar();
get_footer();
<br> <br>
<br> <br>
+32 -27
View File
@@ -1,12 +1,13 @@
{ {
"name": "underscores", "name": "cyywordpress",
"version": "1.0.0", "version": "2.0.0",
"description": "Hi. I'm a starter theme called _s, or underscores, if you like. I'm a theme meant for hacking so don't use me as a Parent Theme. Instead try turning me into the next, most awesome, WordPress theme out there. That's what I'm here for.", "description": "CyyWordpress a modern WordPress theme built on Bulma 1.x and Underscores.",
"author": "Automattic Theme Team", "author": "Chuyaoyuan",
"license": "GPL-2.0-or-later", "license": "GPL-2.0-or-later",
"keywords": [ "keywords": [
"WordPress", "WordPress",
"Theme" "Theme",
"Bulma"
], ],
"homepage": "https://github.com/Automattic/_s#readme", "homepage": "https://github.com/Automattic/_s#readme",
"repository": { "repository": {
@@ -17,30 +18,34 @@
"url": "https://github.com/Automattic/_s/issues" "url": "https://github.com/Automattic/_s/issues"
}, },
"devDependencies": { "devDependencies": {
"@wordpress/scripts": "^12.1.0", "@fortawesome/fontawesome-free": "^6.5.2",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@wordpress/eslint-plugin": "^21.0.0",
"@wordpress/scripts": "^30.0.0",
"autoprefixer": "^10.4.19",
"bulma": "^1.0.2",
"cssnano": "^7.0.0",
"dir-archiver": "^1.1.1", "dir-archiver": "^1.1.1",
"node-sass": "^4.14.1", "eslint": "^8.57.0",
"rtlcss": "^2.5.0" "postcss": "^8.4.38",
}, "rtlcss": "^4.3.0",
"rtlcssConfig": { "sass": "^1.77.0",
"options": { "stylelint": "^16.6.0",
"autoRename": false, "stylelint-config-standard-scss": "^14.0.0",
"autoRenameStrict": false, "typescript": "^5.4.5",
"blacklist": {}, "vite": "^5.2.11",
"clean": true, "vite-plugin-sass-glob-import": "^1.0.0"
"greedy": false,
"processUrls": false,
"stringMap": []
},
"plugins": [],
"map": false
}, },
"scripts": { "scripts": {
"watch": "node-sass sass/ -o ./ --source-map true --output-style expanded --indent-type tab --indent-width 1 -w", "dev": "vite",
"compile:css": "node-sass sass/ -o ./ && stylelint '*.css' --fix || true && stylelint '*.css' --fix", "build": "vite build",
"compile:rtl": "rtlcss style.css style-rtl.css", "build:rtl": "rtlcss assets/css/main.css assets/css/main-rtl.css",
"lint:scss": "wp-scripts lint-style 'sass/**/*.scss'", "lint": "eslint 'src/**/*.{ts,js}' && stylelint 'scss/**/*.scss'",
"lint:js": "wp-scripts lint-js 'js/*.js'", "lint:js": "wp-scripts lint-js 'src/**/*.ts'",
"bundle": "dir-archiver --src . --dest ../_s.zip --exclude .DS_Store .stylelintrc.json .eslintrc .git .gitattributes .github .gitignore README.md composer.json composer.lock node_modules vendor package-lock.json package.json .travis.yml phpcs.xml.dist sass style.css.map" "lint:scss": "stylelint 'scss/**/*.scss'",
"type-check": "tsc --noEmit",
"bundle": "dir-archiver --src . --dest ../cyywordpress.zip --exclude .DS_Store .stylelintrc.json .eslintrc.json .git .gitattributes .github .gitignore composer.json composer.lock node_modules vendor package-lock.json package.json phpcs.xml.dist scss src tsconfig.json vite.config.ts postcss.config.js"
} }
} }
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* Block Pattern: CTA Section
*
* @package CyyWordpress
*/
register_block_pattern(
'cyywordpress/cta-section',
array(
'title' => esc_html__( '行动号召区域', 'cyywordpress' ),
'description' => esc_html__( '中心对齐的行动号召区域,含标题、描述和按钮。', 'cyywordpress' ),
'categories' => array( 'cyywordpress' ),
'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var:preset|spacing|70","bottom":"var:preset|spacing|70"}},"color":{"background":"var:preset|color|light"}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull has-background" style="background-color:var(--wp--preset--color--light);padding-top:var(--wp--preset--spacing--70);padding-bottom:var(--wp--preset--spacing--70)">
<!-- wp:group {"layout":{"type":"flex","orientation":"vertical","justifyContent":"center"}} -->
<div class="wp-block-group">
<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">' . esc_html__( '准备好开始了吗?', 'cyywordpress' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"1.125rem"}}} -->
<p class="has-text-align-center" style="font-size:1.125rem">' . esc_html__( '加入我们,探索更多精彩内容。', 'cyywordpress' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"},"style":{"spacing":{"margin":{"top":"var:preset|spacing|40"}}}} -->
<div class="wp-block-buttons" style="margin-top:var(--wp--preset--spacing--40)">
<!-- wp:button {"backgroundColor":"primary","textColor":"white","style":{"border":{"radius":"6px"}}} -->
<div class="wp-block-button"><a class="wp-block-button__link has-primary-background-color has-white-color has-text-color has-background wp-element-button" style="border-radius:6px">' . esc_html__( '立即开始', 'cyywordpress' ) . '</a></div>
<!-- /wp:button -->
<!-- wp:button {"className":"is-style-outline","style":{"border":{"radius":"6px"}}} -->
<div class="wp-block-button is-style-outline"><a class="wp-block-button__link wp-element-button" style="border-radius:6px">' . esc_html__( '了解更多', 'cyywordpress' ) . '</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->',
)
);
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* Block Pattern: Hero Banner
*
* @package CyyWordpress
*/
register_block_pattern(
'cyywordpress/hero-banner',
array(
'title' => esc_html__( 'Hero 横幅', 'cyywordpress' ),
'description' => esc_html__( '大标题 + 副标题 + 行动号召按钮的 Hero 区域。', 'cyywordpress' ),
'categories' => array( 'cyywordpress' ),
'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var:preset|spacing|70","bottom":"var:preset|spacing|70"}},"color":{"gradient":"linear-gradient(135deg,#00d1b2 0%,#3e8ed0 100%)"}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull has-background" style="padding-top:var(--wp--preset--spacing--70);padding-bottom:var(--wp--preset--spacing--70)">
<!-- wp:group {"layout":{"type":"flex","orientation":"vertical","justifyContent":"center"}} -->
<div class="wp-block-group">
<!-- wp:heading {"textAlign":"center","level":1,"style":{"color":{"text":"#ffffff"},"typography":{"fontSize":"clamp(2rem,5vw,3.5rem)","fontWeight":"700"}}} -->
<h1 class="wp-block-heading has-text-align-center" style="color:#ffffff;font-size:clamp(2rem,5vw,3.5rem);font-weight:700">' . esc_html__( '欢迎来到我的博客', 'cyywordpress' ) . '</h1>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center","style":{"color":{"text":"rgba(255,255,255,0.9)"},"typography":{"fontSize":"1.25rem"}}} -->
<p class="has-text-align-center" style="color:rgba(255,255,255,0.9);font-size:1.25rem">' . esc_html__( '分享技术、生活与思考', 'cyywordpress' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"},"style":{"spacing":{"margin":{"top":"var:preset|spacing|40"}}}} -->
<div class="wp-block-buttons" style="margin-top:var(--wp--preset--spacing--40)">
<!-- wp:button {"backgroundColor":"white","textColor":"primary","style":{"border":{"radius":"6px"}}} -->
<div class="wp-block-button"><a class="wp-block-button__link has-white-background-color has-primary-color has-text-color has-background wp-element-button" style="border-radius:6px">' . esc_html__( '浏览文章', 'cyywordpress' ) . '</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->',
)
);
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* Block Pattern: Post Card Grid
*
* @package CyyWordpress
*/
register_block_pattern(
'cyywordpress/post-card-grid',
array(
'title' => esc_html__( '文章卡片网格', 'cyywordpress' ),
'description' => esc_html__( '响应式三列文章卡片网格布局。', 'cyywordpress' ),
'categories' => array( 'cyywordpress' ),
'content' => '<!-- wp:group {"align":"wide","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignwide">
<!-- wp:heading {"textAlign":"center","style":{"spacing":{"margin":{"bottom":"var:preset|spacing|50"}}}} -->
<h2 class="wp-block-heading has-text-align-center">' . esc_html__( '最新文章', 'cyywordpress' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:columns {"isStackedOnMobile":true,"style":{"spacing":{"blockGap":{"top":"var:preset|spacing|40","left":"var:preset|spacing|40"}}}} -->
<div class="wp-block-columns is-layout-flex wp-container-core-columns">
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:group {"style":{"border":{"radius":"8px","width":"1px","color":"var:preset|color|light"},"spacing":{"padding":{"top":"0","bottom":"var:preset|spacing|40","left":"0","right":"0"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="border-color:var(--wp--preset--color--light);border-style:solid;border-width:1px;border-radius:8px;padding-bottom:var(--wp--preset--spacing--40)">
<!-- wp:image {"sizeSlug":"card-thumbnail","style":{"border":{"radius":{"topLeft":"8px","topRight":"8px"}}}} -->
<figure class="wp-block-image size-card-thumbnail" style="border-top-left-radius:8px;border-top-right-radius:8px"><img alt="" /></figure>
<!-- /wp:image -->
<!-- wp:group {"style":{"spacing":{"padding":{"left":"var:preset|spacing|40","right":"var:preset|spacing|40"}}}} -->
<div class="wp-block-group" style="padding-left:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40)">
<!-- wp:heading {"level":3,"style":{"spacing":{"margin":{"top":"var:preset|spacing|30"}}}} -->
<h3 class="wp-block-heading" style="margin-top:var(--wp--preset--spacing--30)">' . esc_html__( '文章标题', 'cyywordpress' ) . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"style":{"color":{"text":"#6b7280"}}} -->
<p style="color:#6b7280">' . esc_html__( '文章摘要文字,简要介绍内容。', 'cyywordpress' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:read-more {"content":"' . esc_html__( '阅读全文 →', 'cyywordpress' ) . '"} /-->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:group {"style":{"border":{"radius":"8px","width":"1px","color":"var:preset|color|light"},"spacing":{"padding":{"top":"0","bottom":"var:preset|spacing|40","left":"0","right":"0"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="border-color:var(--wp--preset--color--light);border-style:solid;border-width:1px;border-radius:8px;padding-bottom:var(--wp--preset--spacing--40)">
<!-- wp:image {"sizeSlug":"card-thumbnail","style":{"border":{"radius":{"topLeft":"8px","topRight":"8px"}}}} -->
<figure class="wp-block-image size-card-thumbnail" style="border-top-left-radius:8px;border-top-right-radius:8px"><img alt="" /></figure>
<!-- /wp:image -->
<!-- wp:group {"style":{"spacing":{"padding":{"left":"var:preset|spacing|40","right":"var:preset|spacing|40"}}}} -->
<div class="wp-block-group" style="padding-left:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40)">
<!-- wp:heading {"level":3,"style":{"spacing":{"margin":{"top":"var:preset|spacing|30"}}}} -->
<h3 class="wp-block-heading" style="margin-top:var(--wp--preset--spacing--30)">' . esc_html__( '文章标题', 'cyywordpress' ) . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"style":{"color":{"text":"#6b7280"}}} -->
<p style="color:#6b7280">' . esc_html__( '文章摘要文字,简要介绍内容。', 'cyywordpress' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:read-more {"content":"' . esc_html__( '阅读全文 →', 'cyywordpress' ) . '"} /-->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:group {"style":{"border":{"radius":"8px","width":"1px","color":"var:preset|color|light"},"spacing":{"padding":{"top":"0","bottom":"var:preset|spacing|40","left":"0","right":"0"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="border-color:var(--wp--preset--color--light);border-style:solid;border-width:1px;border-radius:8px;padding-bottom:var(--wp--preset--spacing--40)">
<!-- wp:image {"sizeSlug":"card-thumbnail","style":{"border":{"radius":{"topLeft":"8px","topRight":"8px"}}}} -->
<figure class="wp-block-image size-card-thumbnail" style="border-top-left-radius:8px;border-top-right-radius:8px"><img alt="" /></figure>
<!-- /wp:image -->
<!-- wp:group {"style":{"spacing":{"padding":{"left":"var:preset|spacing|40","right":"var:preset|spacing|40"}}}} -->
<div class="wp-block-group" style="padding-left:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40)">
<!-- wp:heading {"level":3,"style":{"spacing":{"margin":{"top":"var:preset|spacing|30"}}}} -->
<h3 class="wp-block-heading" style="margin-top:var(--wp--preset--spacing--30)">' . esc_html__( '文章标题', 'cyywordpress' ) . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"style":{"color":{"text":"#6b7280"}}} -->
<p style="color:#6b7280">' . esc_html__( '文章摘要文字,简要介绍内容。', 'cyywordpress' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:read-more {"content":"' . esc_html__( '阅读全文 →', 'cyywordpress' ) . '"} /-->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
</div>
<!-- /wp:group -->',
)
);
+7 -3
View File
@@ -61,7 +61,7 @@
Multiple valid text domains can be provided as a comma-delimited list. --> Multiple valid text domains can be provided as a comma-delimited list. -->
<rule ref="WordPress.WP.I18n"> <rule ref="WordPress.WP.I18n">
<properties> <properties>
<property name="text_domain" type="array" value="_s"/> <property name="text_domain" type="array" value="cyywordpress"/>
</properties> </properties>
</rule> </rule>
@@ -76,7 +76,11 @@
<!-- Set the minimum supported WP version. This is used by several sniffs. <!-- Set the minimum supported WP version. This is used by several sniffs.
The minimum version set here should be in line with the minimum WP version The minimum version set here should be in line with the minimum WP version
as set in the "Requires at least" tag in the readme.txt file. --> as set in the "Requires at least" tag in the readme.txt file. -->
<config name="minimum_supported_wp_version" value="4.5"/> <config name="minimum_supported_wp_version" value="6.0"/>
<!-- PHP 8.1 compatibility check. -->
<config name="testVersion" value="8.1-"/>
<rule ref="PHPCompatibilityWP"/>
<rule ref="WordPress.Arrays.MultipleStatementAlignment"> <rule ref="WordPress.Arrays.MultipleStatementAlignment">
<properties> <properties>
@@ -93,7 +97,7 @@
Multiple valid prefixes can be provided as a comma-delimited list. --> Multiple valid prefixes can be provided as a comma-delimited list. -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals"> <rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties> <properties>
<property name="prefixes" type="array" value="_s" /> <property name="prefixes" type="array" value="cyywordpress,CyyWordpress,Bulmapress" />
</properties> </properties>
</rule> </rule>
+9
View File
@@ -0,0 +1,9 @@
/** @type {import('postcss-load-config').Config} */
module.exports = {
plugins: [
require( 'autoprefixer' ),
...( process.env.NODE_ENV === 'production'
? [ require( 'cssnano' )( { preset: 'default' } ) ]
: [] ),
],
};
+27
View File
@@ -0,0 +1,27 @@
// =============================================================================
// abstracts/_functions.scss
// Pure SCSS helper functions.
// =============================================================================
// Convert px value to rem.
@function rem($px, $base: 16) {
@return #{$px / $base}rem;
}
// Strip units from a value.
@function strip-unit($value) {
@return $value / ($value * 0 + 1);
}
// Fluid type scale using clamp().
// Usage: font-size: fluid-type(16px, 24px);
@function fluid-type($min-size, $max-size, $min-vw: 375px, $max-vw: 1280px) {
$min-rem: rem(strip-unit($min-size));
$max-rem: rem(strip-unit($max-size));
@return clamp(
#{$min-rem},
calc(#{$min-rem} + (#{strip-unit($max-size)} - #{strip-unit($min-size)}) * ((100vw - #{$min-vw}) / (#{strip-unit($max-vw)} - #{strip-unit($min-vw)}))),
#{$max-rem}
);
}
+100
View File
@@ -0,0 +1,100 @@
// =============================================================================
// abstracts/_mixins.scss
// Responsive helpers and reusable patterns.
// =============================================================================
// Bulma-matching breakpoint mixins.
@mixin mobile {
@media screen and (max-width: 768px) { @content; }
}
@mixin tablet {
@media screen and (min-width: 769px) { @content; }
}
@mixin tablet-only {
@media screen and (min-width: 769px) and (max-width: 1023px) { @content; }
}
@mixin desktop {
@media screen and (min-width: 1024px) { @content; }
}
@mixin widescreen {
@media screen and (min-width: 1216px) { @content; }
}
// Truncate text to N lines using -webkit-line-clamp.
@mixin line-clamp($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
// Visually hidden (accessible).
@mixin visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
// Aspect-ratio box.
@mixin aspect-ratio($w, $h) {
position: relative;
&::before {
content: '';
display: block;
padding-top: percentage($h / $w);
}
> * {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
// Focus visible ring (WCAG 2.1).
@mixin focus-ring($color: var(--color-primary)) {
&:focus-visible {
outline: 3px solid $color;
outline-offset: 2px;
}
}
// Card elevation with hover lift.
@mixin card-lift {
transition: transform var(--transition-base), box-shadow var(--transition-base);
&:hover {
transform: translateY(-4px);
box-shadow: var(--card-shadow-hover);
}
}
// Skeleton loading shimmer.
@mixin skeleton-shimmer {
background: linear-gradient(
90deg,
var(--color-surface) 25%,
rgba(255, 255, 255, 0.5) 50%,
var(--color-surface) 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
+79
View File
@@ -0,0 +1,79 @@
// =============================================================================
// abstracts/_variables.scss
// CSS Custom Properties + SCSS variables for the 7-1 architecture.
// Bulma 1.x is built on CSS variables, so we map our design tokens here.
// =============================================================================
// --- Colour tokens -----------------------------------------------------------
:root {
--color-primary: #00d1b2;
--color-primary-dark: #00a896;
--color-dark: #363636;
--color-light: #f5f5f5;
--color-white: #ffffff;
--color-danger: #f14668;
--color-warning: #ffe08a;
--color-info: #3e8ed0;
--color-success: #48c78e;
// Semantic surface tokens (flipped in dark mode).
--color-background: #ffffff;
--color-text: #363636;
--color-surface: #f5f5f5;
--color-border: #dbdbdb;
--color-shadow: rgba(0, 0, 0, 0.1);
// Navbar
--navbar-bg: rgba(255, 255, 255, 0.85);
--navbar-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
// Card
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--card-shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.14);
--card-radius: 8px;
// Typography
--font-family-base: 'Noto Sans SC', system-ui, -apple-system, sans-serif;
--font-size-base: 1rem;
--line-height-base: 1.7;
// Spacing scale (mirrors theme.json)
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2rem;
--space-6: 3rem;
--space-7: 4.5rem;
// Breakpoints (Bulma defaults)
--bp-mobile: 768px;
--bp-tablet: 769px;
--bp-desktop: 1024px;
--bp-widescreen: 1216px;
// Transitions
--transition-base: 0.3s ease;
}
// Dark mode overrides via data-attribute or media query.
[data-theme='dark'],
@media (prefers-color-scheme: dark) {
:root {
--color-background: #1a1a2e;
--color-text: #e0e0e0;
--color-surface: #16213e;
--color-border: #444444;
--color-shadow: rgba(0, 0, 0, 0.4);
--navbar-bg: rgba(26, 26, 46, 0.9);
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
--card-shadow-hover:0 8px 24px rgba(0, 0, 0, 0.5);
}
}
// SCSS-only convenience aliases (not emitted as CSS).
$primary: #00d1b2 !default;
$primary-dark: #00a896 !default;
$dark: #363636 !default;
$font-base: var(--font-family-base);
$transition: var(--transition-base);
+56
View File
@@ -0,0 +1,56 @@
// =============================================================================
// base/_reset.scss
// Minimal modern reset on top of Bulma's built-in normalisation.
// =============================================================================
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
text-size-adjust: 100%;
}
body {
background-color: var(--color-background);
color: var(--color-text);
font-family: var(--font-family-base);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
transition:
background-color var(--transition-base),
color var(--transition-base);
}
img,
video,
svg {
max-width: 100%;
height: auto;
}
a {
color: var(--color-primary);
text-underline-offset: 0.2em;
&:hover {
color: var(--color-primary-dark);
}
@include focus-ring;
}
button {
cursor: pointer;
}
// Remove default list style for navigation lists.
nav ul,
nav ol {
list-style: none;
margin: 0;
padding: 0;
}
+58
View File
@@ -0,0 +1,58 @@
// =============================================================================
// base/_typography.scss
// =============================================================================
h1, h2, h3, h4, h5, h6 {
line-height: 1.3;
font-weight: 700;
color: var(--color-dark);
}
p {
margin-bottom: var(--space-3);
}
code,
pre {
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.875em;
}
pre {
overflow-x: auto;
padding: var(--space-3);
background: var(--color-surface);
border-radius: var(--card-radius);
border-left: 4px solid var(--color-primary);
code {
background: transparent;
padding: 0;
}
}
blockquote {
border-left: 4px solid var(--color-primary);
padding-left: var(--space-3);
margin-left: 0;
color: #6b7280;
font-style: italic;
}
.screen-reader-text {
@include visually-hidden;
&:focus {
background-color: var(--color-background);
clip: auto;
height: auto;
left: var(--space-3);
top: var(--space-3);
width: auto;
z-index: 100000;
padding: var(--space-2) var(--space-3);
border: 2px solid var(--color-primary);
border-radius: 4px;
font-weight: 600;
}
}
+38
View File
@@ -0,0 +1,38 @@
// =============================================================================
// components/_button.scss
// =============================================================================
// Extend Bulma's .button with our colour tokens.
.button.is-primary {
background-color: var(--color-primary);
border-color: transparent;
color: #fff;
&:hover {
background-color: var(--color-primary-dark);
}
}
// Copy-code button on code blocks.
.copy-code-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: #e0e0e0;
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
cursor: pointer;
transition: background var(--transition-base);
&:hover {
background: rgba(255, 255, 255, 0.2);
}
&.is-copied {
color: var(--color-success);
border-color: var(--color-success);
}
}
+168
View File
@@ -0,0 +1,168 @@
// =============================================================================
// components/_card.scss
// Post card component.
// =============================================================================
.post-card {
border-radius: var(--card-radius);
overflow: hidden;
background: var(--color-background);
border: 1px solid var(--color-border);
box-shadow: var(--card-shadow);
margin-bottom: var(--space-4);
transition:
transform var(--transition-base),
box-shadow var(--transition-base);
&:hover {
transform: translateY(-4px);
box-shadow: var(--card-shadow-hover);
}
// Featured image wrapper.
&__image {
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
}
&:hover &__image img {
transform: scale(1.03);
}
// Gradient placeholder when no featured image.
&__gradient-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--color-primary), var(--color-info));
display: flex;
align-items: center;
justify-content: center;
}
&__category-initial {
font-size: 2.5rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.6);
}
// Categories.
&__categories {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-bottom: var(--space-2);
}
&__category-badge {
font-size: 0.75rem;
text-decoration: none;
border-radius: 4px;
transition: opacity var(--transition-base);
&:hover {
opacity: 0.8;
}
}
// Title.
&__title {
margin-bottom: var(--space-2) !important;
a {
color: var(--color-text);
text-decoration: none;
transition: color var(--transition-base);
&:hover {
color: var(--color-primary);
}
}
}
// Meta row.
&__meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
color: #6b7280;
font-size: 0.85rem;
margin-bottom: var(--space-3) !important;
a {
color: #6b7280;
&:hover {
color: var(--color-primary);
}
}
i {
margin-right: 0.25rem;
}
}
// Footer row.
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-3);
}
// "Continue reading" button arrow animation.
&__read-more {
display: inline-flex;
align-items: center;
gap: 0.25rem;
text-decoration: none;
transition:
background-color var(--transition-base),
color var(--transition-base);
}
&__read-more-arrow {
display: inline-block;
transition: transform var(--transition-base);
}
&__read-more:hover &__read-more-arrow {
transform: translateX(4px);
}
// Hero variant (sticky posts).
&--hero {
.post-card__image {
max-height: 400px;
}
.post-card__title {
font-size: 1.75rem;
}
}
// Skeleton loading state.
&--loading {
pointer-events: none;
.post-card__image,
.post-card__title,
.post-card__meta,
.post-card__content {
border-radius: 4px;
@include skeleton-shimmer;
color: transparent;
}
.post-card__image {
height: 180px;
}
}
}
+89
View File
@@ -0,0 +1,89 @@
// =============================================================================
// components/_navigation.scss
// Bulma navbar customisation + mobile dropdown corrections.
// =============================================================================
// Navbar item links.
.navbar-item,
.navbar-link {
color: var(--color-text);
transition: color var(--transition-base), background-color var(--transition-base);
&:hover,
&:focus-visible {
background-color: transparent;
color: var(--color-primary);
}
}
// Dropdown.
.navbar-dropdown {
background: var(--color-background);
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 8px 8px;
box-shadow: var(--card-shadow);
.navbar-item {
padding: 0.5rem 1rem;
&:hover {
background-color: var(--color-surface);
}
}
.navbar-divider {
background-color: var(--color-border);
}
}
// Breadcrumb.
.breadcrumb {
margin: var(--space-3) 0;
font-size: 0.9rem;
a {
color: var(--color-text);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
transition: color var(--transition-base);
&:hover {
color: var(--color-primary);
}
}
li.is-active a {
color: #6b7280;
}
}
// Post navigation.
.nav-links {
.nav-previous,
.nav-next {
a {
display: inline-flex;
flex-direction: column;
text-decoration: none;
max-width: 300px;
color: var(--color-text);
transition: color var(--transition-base);
&:hover {
color: var(--color-primary);
}
}
}
.nav-subtitle {
font-size: 0.8rem;
color: #6b7280;
}
.nav-title {
font-weight: 600;
}
}
+66
View File
@@ -0,0 +1,66 @@
// =============================================================================
// components/_toc.scss
// Article Table of Contents sidebar widget.
// =============================================================================
.toc {
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--card-radius);
position: sticky;
top: 80px;
max-height: calc(100vh - 120px);
overflow-y: auto;
scrollbar-width: thin;
.card-header {
background: transparent;
border-bottom: 1px solid var(--color-border);
}
.card-header-title {
font-size: 0.9rem;
color: var(--color-text);
}
.card-content {
padding: var(--space-2);
}
}
#toc-list {
ol {
list-style: decimal;
padding-left: var(--space-4);
margin: 0;
}
li {
margin-bottom: var(--space-2);
font-size: 0.85rem;
}
a {
color: var(--color-text);
text-decoration: none;
line-height: 1.4;
transition: color var(--transition-base);
display: block;
&:hover,
&.toc-active {
color: var(--color-primary);
}
&.toc-active {
font-weight: 600;
}
}
// Second-level (h3).
.toc-level-3 {
padding-left: var(--space-3);
list-style: disc;
color: #6b7280;
}
}
+106
View File
@@ -0,0 +1,106 @@
// =============================================================================
// layout/_footer.scss
// =============================================================================
.site-footer {
margin-top: var(--space-6);
background-color: var(--color-surface);
border-top: 1px solid var(--color-border);
padding-top: var(--space-5);
}
.footer-site-name {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: var(--space-2);
a {
color: var(--color-text);
text-decoration: none;
&:hover {
color: var(--color-primary);
}
}
}
.footer-site-description {
color: #6b7280;
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: var(--space-3);
}
.footer-menu-list {
list-style: none;
margin: 0;
padding: 0;
li {
margin-bottom: var(--space-2);
}
a {
color: var(--color-text);
text-decoration: none;
transition: color var(--transition-base);
&:hover {
color: var(--color-primary);
}
}
}
.social-menu-list {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
list-style: none;
margin: 0;
padding: 0;
a {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
background-color: var(--color-border);
color: var(--color-text);
transition:
background-color var(--transition-base),
color var(--transition-base);
&:hover {
background-color: var(--color-primary);
color: #fff;
}
}
}
.footer-widget-title {
font-size: 1rem;
font-weight: 700;
margin-bottom: var(--space-3);
color: var(--color-text);
}
.footer-bottom {
margin-top: var(--space-5);
padding: var(--space-3) 0;
border-top: 1px solid var(--color-border);
}
.footer-copyright {
color: #6b7280;
font-size: 0.875rem;
a {
color: var(--color-text);
&:hover {
color: var(--color-primary);
}
}
}
+66
View File
@@ -0,0 +1,66 @@
// =============================================================================
// layout/_grid.scss
// Site-wide layout helpers.
// =============================================================================
.site-content {
padding-top: var(--space-4);
padding-bottom: var(--space-6);
}
// Primary content area + sidebar layout.
.content-area {
@include desktop {
display: flex;
gap: var(--space-5);
#main {
flex: 1;
min-width: 0; // Prevent flex blowout.
}
}
}
// Responsive post grid.
.posts-grid {
margin-bottom: var(--space-4);
}
// Pagination.
.posts-pagination {
margin-top: var(--space-4);
display: flex;
justify-content: center;
.nav-links {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
justify-content: center;
}
a,
span {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
padding: 0 var(--space-2);
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-text);
text-decoration: none;
transition:
background-color var(--transition-base),
color var(--transition-base),
border-color var(--transition-base);
&:hover,
&.current {
background-color: var(--color-primary);
color: #fff;
border-color: var(--color-primary);
}
}
}
+192
View File
@@ -0,0 +1,192 @@
// =============================================================================
// layout/_header.scss
// =============================================================================
.site-navbar {
position: sticky;
top: 0;
z-index: 100;
background-color: var(--navbar-bg);
box-shadow: var(--navbar-shadow);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: box-shadow var(--transition-base), background-color var(--transition-base);
&.is-scrolled {
box-shadow: 0 4px 12px var(--color-shadow);
}
}
// Logo area.
.navbar-logo {
padding: 0.5rem 0.75rem;
img {
max-height: 2.5rem;
width: auto;
}
}
.navbar-site-name {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
color: var(--color-dark) !important;
white-space: nowrap;
}
.site-title-initial {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: var(--color-primary);
color: #fff;
font-weight: 700;
font-size: 1rem;
}
// Hamburger burger three-line → X animation.
.navbar-burger {
background: transparent;
border: none;
padding: 0.5rem;
transition: opacity var(--transition-base);
span {
display: block;
width: 1.25rem;
height: 2px;
background: var(--color-text);
margin: 4px 0;
transition:
transform 0.3s ease,
opacity 0.3s ease;
transform-origin: center;
}
&.is-active span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
&.is-active span:nth-child(2) { opacity: 0; }
&.is-active span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
}
// Search widget.
.navbar-search-wrap {
position: relative;
}
.navbar-search-toggle {
background: transparent;
border: none;
color: var(--color-text);
font-size: 1rem;
padding: 0.25rem 0.5rem;
transition: color var(--transition-base);
&:hover {
color: var(--color-primary);
}
@include focus-ring;
}
.navbar-search-form {
position: absolute;
top: 110%;
right: 0;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 0.5rem;
box-shadow: var(--card-shadow);
min-width: 200px;
.input {
border-color: var(--color-border);
background: var(--color-background);
color: var(--color-text);
}
}
// Dark-mode toggle.
.theme-toggle {
background: transparent;
border: none;
font-size: 1rem;
color: var(--color-text);
padding: 0.25rem 0.5rem;
transition: color var(--transition-base);
line-height: 1;
&:hover {
color: var(--color-primary);
}
@include focus-ring;
}
// Show only the relevant icon.
body:not([data-theme='dark']) .theme-toggle__light-icon { display: none; }
[data-theme='dark'] .theme-toggle__dark-icon { display: none; }
// Back-to-top button.
.back-to-top {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 200;
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: var(--color-primary);
color: #fff;
border: none;
font-size: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--card-shadow);
transition:
opacity var(--transition-base),
transform var(--transition-base),
box-shadow var(--transition-base);
opacity: 0;
transform: translateY(1rem);
pointer-events: none;
&.is-visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
&:hover {
box-shadow: var(--card-shadow-hover);
}
@include focus-ring;
}
// Skip link.
.skip-link {
@include visually-hidden;
&:focus {
position: fixed;
top: var(--space-2);
left: var(--space-2);
z-index: 9999;
background: var(--color-primary);
color: #fff;
padding: var(--space-2) var(--space-3);
border-radius: 4px;
clip: auto;
height: auto;
width: auto;
font-weight: 700;
text-decoration: none;
}
}
+26
View File
@@ -0,0 +1,26 @@
// =============================================================================
// layout/_sidebar.scss
// =============================================================================
.widget-area {
.widget {
border-radius: var(--card-radius);
overflow: hidden;
.card-content {
padding-top: var(--space-3);
}
}
}
// Sticky sidebar on desktop.
@include desktop {
#secondary.widget-area {
position: sticky;
top: calc(var(--space-5) + 60px); // 60px = approx navbar height.
align-self: flex-start;
max-height: calc(100vh - 100px);
overflow-y: auto;
scrollbar-width: thin;
}
}
+35
View File
@@ -0,0 +1,35 @@
// =============================================================================
// main.scss CyyWordpress 7-1 SCSS entry point
// =============================================================================
// 1. Abstracts (no CSS output, just variables / mixins / functions)
@use 'abstracts/variables';
@use 'abstracts/mixins' as *;
@use 'abstracts/functions';
// 2. Third-party framework (selective Bulma import)
@use 'vendors/bulma';
// 3. Base styles
@use 'base/reset';
@use 'base/typography';
// 4. Layout shells
@use 'layout/grid';
@use 'layout/header';
@use 'layout/footer';
@use 'layout/sidebar';
// 5. Reusable components
@use 'components/card';
@use 'components/navigation';
@use 'components/button';
@use 'components/toc';
// 6. Page-specific
@use 'pages/home';
@use 'pages/single';
@use 'pages/archive';
// 7. Themes (dark mode overrides)
@use 'themes/dark';
+18
View File
@@ -0,0 +1,18 @@
// =============================================================================
// pages/_archive.scss
// =============================================================================
.archive-header {
margin-bottom: var(--space-4);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--color-border);
.page-title {
font-size: 1.75rem;
font-weight: 700;
}
.archive-description {
color: #6b7280;
}
}
+8
View File
@@ -0,0 +1,8 @@
// =============================================================================
// pages/_home.scss
// =============================================================================
.welcome-banner {
margin-bottom: var(--space-3);
border-radius: var(--card-radius);
}
+181
View File
@@ -0,0 +1,181 @@
// =============================================================================
// pages/_single.scss
// =============================================================================
// Reading progress bar.
.reading-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 3px;
z-index: 9999;
background: transparent;
pointer-events: none;
&__bar {
height: 100%;
width: 0;
background: var(--color-primary);
transition: width 0.1s linear;
}
}
// Hero featured image.
.single-hero {
position: relative;
width: 100%;
max-height: 450px;
overflow: hidden;
border-radius: var(--card-radius);
margin-bottom: var(--space-4);
&__image {
width: 100%;
height: 450px;
object-fit: cover;
display: block;
loading: eager;
}
&__overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.65) 0%, transparent 60%);
display: flex;
align-items: flex-end;
padding: var(--space-5);
}
&__title {
color: #fff;
font-size: clamp(1.5rem, 4vw, 2.5rem);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
margin: 0;
}
}
// Single article layout.
.single-layout {
align-items: flex-start;
}
// Social share.
.social-share {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
margin: var(--space-4) 0;
padding: var(--space-3);
background: var(--color-surface);
border-radius: var(--card-radius);
border: 1px solid var(--color-border);
&__label {
font-size: 0.875rem;
color: #6b7280;
}
&__btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
background: var(--color-border);
color: var(--color-text);
border: none;
font-size: 1rem;
text-decoration: none;
transition:
background-color var(--transition-base),
color var(--transition-base);
&:hover {
background-color: var(--color-primary);
color: #fff;
}
&--weibo:hover { background-color: #e6162d; }
&--twitter:hover { background-color: #000; }
}
}
// Author bio.
.author-bio {
margin: var(--space-4) 0;
border-radius: var(--card-radius);
border: 1px solid var(--color-border);
.media {
align-items: flex-start;
}
&__avatar {
border-radius: 50%;
}
&__name {
display: block;
color: var(--color-text);
text-decoration: none;
margin-bottom: var(--space-2);
&:hover { color: var(--color-primary); }
}
&__description {
font-size: 0.9rem;
color: #6b7280;
line-height: 1.6;
}
}
// Related posts.
.related-posts {
margin: var(--space-4) 0;
&__title {
margin-bottom: var(--space-3);
}
}
.related-post-card {
height: 100%;
border-radius: var(--card-radius);
border: 1px solid var(--color-border);
overflow: hidden;
transition:
transform var(--transition-base),
box-shadow var(--transition-base);
&:hover {
transform: translateY(-2px);
box-shadow: var(--card-shadow-hover);
}
&__image {
width: 100%;
height: 100%;
object-fit: cover;
}
&__title {
display: block;
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text);
text-decoration: none;
line-height: 1.4;
&:hover { color: var(--color-primary); }
}
}
// Code block copy button wrapper.
.entry-content pre,
.post-card__content pre {
position: relative;
}
+42
View File
@@ -0,0 +1,42 @@
// =============================================================================
// themes/_dark.scss
// Dark mode extra overrides beyond CSS custom properties.
// =============================================================================
[data-theme='dark'],
@media (prefers-color-scheme: dark) {
// Bulma component overrides.
.navbar {
background-color: var(--navbar-bg);
border-bottom: 1px solid var(--color-border);
}
.card {
background-color: var(--color-surface);
border-color: var(--color-border);
}
.notification.is-primary {
background-color: rgba(0, 209, 178, 0.15);
color: #00d1b2;
}
.input,
.select select,
.textarea {
background-color: var(--color-surface);
border-color: var(--color-border);
color: var(--color-text);
}
.footer {
background-color: var(--color-surface);
}
// Code blocks.
pre,
code {
background-color: #0d1117;
color: #e6edf3;
}
}
+10
View File
@@ -0,0 +1,10 @@
// =============================================================================
// vendors/_bulma.scss
// Selective Bulma 1.x import only import what the theme uses.
// Bulma 1.x exposes sass/ subdirectory; each module uses @use.
// =============================================================================
@use 'bulma/sass' with (
$primary: #00d1b2,
$dark: #363636,
$light: #f5f5f5
);
+6 -3
View File
@@ -1,6 +1,9 @@
<?php <?php
/** /**
* The sidebar containing the main widget area * The sidebar containing the main widget area.
*
* Fixed: each widget is wrapped by the correct Bulma card structure
* registered in functions.php (before_widget / after_widget).
* *
* @link https://developer.wordpress.org/themes/basics/template-files/#template-partials * @link https://developer.wordpress.org/themes/basics/template-files/#template-partials
* *
@@ -12,7 +15,7 @@ if ( ! is_active_sidebar( 'sidebar-1' ) ) {
} }
?> ?>
<div class="columns" style="margin-bottom: 10px;"> <aside id="secondary" class="widget-area column is-3" role="complementary" aria-label="<?php esc_attr_e( '侧边栏', 'cyywordpress' ); ?>">
<?php dynamic_sidebar( 'sidebar-1' ); ?> <?php dynamic_sidebar( 'sidebar-1' ); ?>
</aside><!-- #secondary -->
</div>
+203 -26
View File
@@ -1,6 +1,15 @@
<?php <?php
/** /**
* The template for displaying all single posts * The template for displaying single posts
*
* Features:
* Full-width hero featured image with title overlay
* Reading progress bar (filled by reading-progress.ts)
* Auto-generated TOC (filled by toc.ts)
* Author bio card
* Related posts (same category)
* Social share buttons (no third-party SDK)
* Breadcrumb, prev/next navigation, comments
* *
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#single-post * @link https://developer.wordpress.org/themes/basics/template-hierarchy/#single-post
* *
@@ -9,56 +18,224 @@
get_header(); get_header();
?> ?>
<br>
<div class="notification is-primary"> <!-- Reading progress bar -->
欢迎来到这! <strong>Hello everyone.</strong> <div id="reading-progress" class="reading-progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="<?php esc_attr_e( '阅读进度', 'cyywordpress' ); ?>">
</div> <div class="reading-progress__bar"></div>
<nav class="breadcrumb" aria-label="breadcrumbs"> </div>
<div id="primary" class="content-area">
<main id="main" class="site-main" role="main">
<nav class="breadcrumb" aria-label="<?php esc_attr_e( '面包屑导航', 'cyywordpress' ); ?>">
<ul> <ul>
<li> <li>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" > <a href="<?php echo esc_url( home_url( '/' ) ); ?>">
<span class="icon is-small"> <span class="icon is-small"><i class="fa-solid fa-house" aria-hidden="true"></i></span>
<i class="fas fa-home" aria-hidden="true"></i> <span><?php esc_html_e( '首页', 'cyywordpress' ); ?></span>
</span>
<span>首页</span>
</a> </a>
</li> </li>
<li> <li class="is-active">
<a href="#"> <a href="#" aria-current="page">
<span class="icon is-small"> <span class="icon is-small"><i class="fa-solid fa-file-lines" aria-hidden="true"></i></span>
<i class="fas fa-book" aria-hidden="true"></i> <span><?php esc_html_e( '文章', 'cyywordpress' ); ?></span>
</span>
<span>文章页</span>
</a> </a>
</li> </li>
</ul> </ul>
</nav> </nav>
<main id="primary" class="site-main">
<?php <?php
while ( have_posts() ) : while ( have_posts() ) :
the_post(); the_post();
?>
get_template_part( 'template-parts/content', get_post_type() ); <?php if ( has_post_thumbnail() ) : ?>
<div class="single-hero">
<?php
the_post_thumbnail(
'full',
array(
'class' => 'single-hero__image',
'loading' => 'eager',
'decoding'=> 'async',
'alt' => the_title_attribute( array( 'echo' => false ) ),
)
);
?>
<div class="single-hero__overlay">
<?php the_title( '<h1 class="single-hero__title">', '</h1>' ); ?>
</div>
</div>
<?php endif; ?>
<div class="columns single-layout">
<!-- TOC sidebar (populated by toc.ts) -->
<aside class="column is-3-desktop is-hidden-touch single-toc" aria-label="<?php esc_attr_e( '目录', 'cyywordpress' ); ?>">
<div id="toc" class="toc card">
<div class="card-header">
<p class="card-header-title">
<i class="fa-solid fa-list" aria-hidden="true"></i>
&nbsp;<?php esc_html_e( '目录', 'cyywordpress' ); ?>
</p>
</div>
<div class="card-content" id="toc-list"></div>
</div>
</aside>
<!-- Main article column -->
<div class="column is-9-desktop single-article">
<?php get_template_part( 'template-parts/content', get_post_type() ); ?>
<!-- Social share -->
<div class="social-share">
<span class="social-share__label"><?php esc_html_e( '分享:', 'cyywordpress' ); ?></span>
<a
class="social-share__btn social-share__btn--weibo"
href="<?php echo esc_url( 'https://service.weibo.com/share/share.php?url=' . rawurlencode( get_permalink() ) . '&title=' . rawurlencode( get_the_title() ) ); ?>"
target="_blank"
rel="noopener noreferrer"
aria-label="<?php esc_attr_e( '分享到微博', 'cyywordpress' ); ?>"
>
<i class="fa-brands fa-weibo" aria-hidden="true"></i>
</a>
<a
class="social-share__btn social-share__btn--twitter"
href="<?php echo esc_url( 'https://twitter.com/intent/tweet?url=' . rawurlencode( get_permalink() ) . '&text=' . rawurlencode( get_the_title() ) ); ?>"
target="_blank"
rel="noopener noreferrer"
aria-label="<?php esc_attr_e( '分享到 Twitter', 'cyywordpress' ); ?>"
>
<i class="fa-brands fa-x-twitter" aria-hidden="true"></i>
</a>
<button
class="social-share__btn social-share__btn--copy"
data-url="<?php echo esc_attr( get_permalink() ); ?>"
aria-label="<?php esc_attr_e( '复制链接', 'cyywordpress' ); ?>"
>
<i class="fa-solid fa-link" aria-hidden="true"></i>
</button>
</div>
<!-- Post navigation -->
<?php
the_post_navigation( the_post_navigation(
array( array(
'prev_text' => '<span class="nav-subtitle">' . esc_html__( '上一篇:', 'cyywordpress' ) . '</span> <span class="nav-title">%title</span>', 'prev_text' => '<span class="nav-subtitle"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> ' . esc_html__( '上一篇', 'cyywordpress' ) . '</span><span class="nav-title">%title</span>',
'next_text' => '<span class="nav-subtitle">' . esc_html__( '下一篇:', 'cyywordpress' ) . '</span> <span class="nav-title">%title</span>', 'next_text' => '<span class="nav-subtitle">' . esc_html__( '下一篇', 'cyywordpress' ) . ' <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></span><span class="nav-title">%title</span>',
)
);
?>
<!-- Author bio -->
<?php
$author_id = (int) get_the_author_meta( 'ID' );
$author_bio = get_the_author_meta( 'description' );
if ( $author_bio ) :
?>
<div class="author-bio card">
<div class="card-content">
<div class="media">
<div class="media-left">
<?php
echo get_avatar(
$author_id,
80,
'',
get_the_author(),
array(
'class' => 'author-bio__avatar',
'loading' => 'lazy',
)
);
?>
</div>
<div class="media-content">
<a class="author-bio__name title is-5" href="<?php echo esc_url( get_author_posts_url( $author_id ) ); ?>">
<?php echo esc_html( get_the_author() ); ?>
</a>
<p class="author-bio__description"><?php echo esc_html( $author_bio ); ?></p>
</div>
</div>
</div>
</div><!-- .author-bio -->
<?php endif; ?>
<!-- Related posts (same first category) -->
<?php
$cats = get_the_category();
if ( $cats ) :
$related = get_posts(
array(
'category__in' => array( $cats[0]->term_id ),
'post__not_in' => array( get_the_ID() ),
'posts_per_page' => 3,
'orderby' => 'rand',
) )
); );
// If comments are open or we have at least one comment, load up the comment template. if ( $related ) :
?>
<div class="related-posts">
<h3 class="related-posts__title title is-5">
<?php esc_html_e( '相关文章', 'cyywordpress' ); ?>
</h3>
<div class="columns is-multiline">
<?php
foreach ( $related as $related_post ) :
setup_postdata( $related_post );
?>
<div class="column is-4">
<div class="card related-post-card">
<?php if ( has_post_thumbnail( $related_post ) ) : ?>
<div class="card-image">
<figure class="image is-16by9">
<?php
echo get_the_post_thumbnail(
$related_post,
'card-thumbnail-sm',
array(
'loading' => 'lazy',
'decoding' => 'async',
'class' => 'related-post-card__image',
'alt' => esc_attr( get_the_title( $related_post ) ),
)
);
?>
</figure>
</div>
<?php endif; ?>
<div class="card-content">
<a class="related-post-card__title" href="<?php echo esc_url( get_permalink( $related_post ) ); ?>">
<?php echo esc_html( get_the_title( $related_post ) ); ?>
</a>
</div>
</div>
</div>
<?php
endforeach;
wp_reset_postdata();
?>
</div><!-- .columns -->
</div><!-- .related-posts -->
<?php endif; ?>
<?php endif; ?>
<!-- Comments -->
<?php
if ( comments_open() || get_comments_number() ) : if ( comments_open() || get_comments_number() ) :
comments_template(); comments_template();
endif; endif;
endwhile; // End of the loop.
?> ?>
</div><!-- .single-article -->
</div><!-- .columns.single-layout -->
<?php endwhile; ?>
</main><!-- #main --> </main><!-- #main -->
</div><!-- #primary -->
<?php <?php
get_sidebar(); get_sidebar();
get_footer(); get_footer();
+45
View File
@@ -0,0 +1,45 @@
/**
* customizer.ts (replaces js/customizer.js)
* Binds postMessage handlers for the WordPress Customizer live preview.
*/
declare const wp: {
customize: {
bind( event: string, callback: () => void ): void;
( settingId: string, callback: ( value: { get(): string; bind( cb: ( v: string ) => void ): void } ) => void ): void;
};
};
( function () {
'use strict';
wp.customize( 'blogname', ( value ) => {
value.bind( ( to: string ) => {
const el = document.querySelector<HTMLElement>( '.site-title a' );
if ( el ) el.textContent = to;
} );
} );
wp.customize( 'blogdescription', ( value ) => {
value.bind( ( to: string ) => {
const el = document.querySelector<HTMLElement>( '.site-description' );
if ( el ) el.textContent = to;
} );
} );
wp.customize( 'header_textcolor', ( value ) => {
value.bind( ( to: string ) => {
const els = document.querySelectorAll<HTMLElement>( '.site-title a, .site-description' );
els.forEach( ( el ) => {
el.style.color = to === 'blank' ? 'transparent' : to;
} );
} );
} );
wp.customize( 'cyywordpress_welcome_text', ( value ) => {
value.bind( ( to: string ) => {
const el = document.querySelector<HTMLElement>( '.welcome-banner' );
if ( el ) el.textContent = to;
} );
} );
} )();
+27
View File
@@ -0,0 +1,27 @@
/**
* back-to-top.ts
* Shows a back-to-top button after scrolling 300 px;
* scrolls to top using smooth behaviour on click.
*/
const SCROLL_THRESHOLD = 300;
export function initBackToTop(): void {
const btn = document.getElementById( 'back-to-top' ) as HTMLButtonElement | null;
if ( ! btn ) return;
// Remove the `hidden` attribute so CSS can control visibility via `.is-visible`.
btn.removeAttribute( 'hidden' );
const onScroll = (): void => {
btn.classList.toggle( 'is-visible', window.scrollY > SCROLL_THRESHOLD );
};
window.addEventListener( 'scroll', onScroll, { passive: true } );
onScroll();
btn.addEventListener( 'click', () => {
window.scrollTo( { top: 0, behavior: 'smooth' } );
btn.blur();
} );
}
+44
View File
@@ -0,0 +1,44 @@
/**
* copy-code.ts
* Adds a "Copy" button to every <pre><code> block in the article content.
*/
export function initCopyCode(): void {
const blocks = document.querySelectorAll<HTMLPreElement>(
'.entry-content pre, .post-card__content pre, .single-article pre'
);
if ( ! blocks.length ) return;
blocks.forEach( ( pre ) => {
// Avoid adding duplicate buttons.
if ( pre.querySelector( '.copy-code-btn' ) ) return;
const btn = document.createElement( 'button' );
btn.className = 'copy-code-btn';
btn.textContent = '复制';
btn.setAttribute( 'aria-label', '复制代码' );
btn.type = 'button';
btn.addEventListener( 'click', async () => {
const code = pre.querySelector( 'code' );
const text = code?.textContent ?? pre.textContent ?? '';
try {
await navigator.clipboard.writeText( text );
btn.textContent = '已复制!';
btn.classList.add( 'is-copied' );
setTimeout( () => {
btn.textContent = '复制';
btn.classList.remove( 'is-copied' );
}, 2000 );
} catch {
btn.textContent = '失败';
setTimeout( () => { btn.textContent = '复制'; }, 2000 );
}
} );
pre.style.position = 'relative';
pre.appendChild( btn );
} );
}
+40
View File
@@ -0,0 +1,40 @@
/**
* lazy-images.ts
* Custom lazy-loading via IntersectionObserver for images
* that use data-src / data-srcset fallback (legacy browsers or pre-rendered markup).
* Native `loading="lazy"` is used for all new images via PHP;
* this module handles any remaining cases.
*/
export function initLazyImages(): void {
if ( ! ( 'IntersectionObserver' in window ) ) return;
const images = document.querySelectorAll<HTMLImageElement>( 'img[data-src]' );
if ( ! images.length ) return;
const observer = new IntersectionObserver(
( entries, obs ) => {
entries.forEach( ( entry ) => {
if ( ! entry.isIntersecting ) return;
const img = entry.target as HTMLImageElement;
if ( img.dataset.src ) {
img.src = img.dataset.src;
delete img.dataset.src;
}
if ( img.dataset.srcset ) {
img.srcset = img.dataset.srcset;
delete img.dataset.srcset;
}
img.classList.add( 'is-loaded' );
obs.unobserve( img );
} );
},
{ rootMargin: '200px 0px' }
);
images.forEach( ( img ) => observer.observe( img ) );
}
+51
View File
@@ -0,0 +1,51 @@
/**
* main.ts
* Entry point conditionally imports each module based on the page context.
*
* Import the SCSS entry point here so Vite bundles both together.
*/
import '@scss/main.scss';
import { initNavigation } from './navigation';
import { initThemeToggle } from './theme-toggle';
import { initBackToTop } from './back-to-top';
import { initLazyImages } from './lazy-images';
// Single-post-only modules (guard by DOM presence to keep bundles lean).
async function loadSingleModules(): Promise<void> {
if ( ! document.querySelector( '.single-article' ) ) return;
const [ { initReadingProgress }, { initToc }, { initCopyCode } ] = await Promise.all( [
import( './reading-progress' ),
import( './toc' ),
import( './copy-code' ),
] );
initReadingProgress();
initToc();
initCopyCode();
// Social share copy link button.
const copyLinkBtns = document.querySelectorAll<HTMLButtonElement>( '.social-share__btn--copy' );
copyLinkBtns.forEach( ( btn ) => {
btn.addEventListener( 'click', async () => {
const url = btn.dataset.url ?? window.location.href;
try {
await navigator.clipboard.writeText( url );
btn.setAttribute( 'aria-label', '链接已复制!' );
setTimeout( () => btn.setAttribute( 'aria-label', '复制链接' ), 2000 );
} catch {
// Clipboard API unavailable silent fail.
}
} );
} );
}
document.addEventListener( 'DOMContentLoaded', () => {
// Apply saved theme before paint to avoid FOUC (also done inline in theme-toggle).
initThemeToggle();
initNavigation();
initBackToTop();
initLazyImages();
void loadSingleModules();
} );
+97
View File
@@ -0,0 +1,97 @@
/**
* navigation.ts
* Handles the Bulma navbar burger toggle + keyboard nav for dropdowns.
*/
interface NavbarBurger extends HTMLElement {
dataset: DOMStringMap & { target?: string };
}
export function initNavigation(): void {
// Hamburger burger toggle.
const burgers = document.querySelectorAll<NavbarBurger>( '.navbar-burger' );
burgers.forEach( ( burger ) => {
burger.addEventListener( 'click', () => {
const targetId = burger.dataset.target ?? burger.getAttribute( 'aria-controls' ) ?? '';
const menu = document.getElementById( targetId );
if ( ! menu ) return;
const isExpanded = burger.getAttribute( 'aria-expanded' ) === 'true';
burger.setAttribute( 'aria-expanded', String( ! isExpanded ) );
burger.classList.toggle( 'is-active' );
menu.classList.toggle( 'is-active' );
} );
} );
// Keyboard support for dropdown menus (Tab / Escape).
const dropdowns = document.querySelectorAll<HTMLElement>( '.has-dropdown' );
dropdowns.forEach( ( dropdown ) => {
const trigger = dropdown.querySelector<HTMLElement>( '.navbar-link' );
const menu = dropdown.querySelector<HTMLElement>( '.navbar-dropdown' );
if ( ! trigger || ! menu ) return;
// Close when Escape is pressed inside the dropdown.
dropdown.addEventListener( 'keydown', ( e: KeyboardEvent ) => {
if ( e.key === 'Escape' ) {
trigger.focus();
dropdown.classList.remove( 'is-active' );
}
} );
// Close when focus leaves the dropdown.
dropdown.addEventListener( 'focusout', ( e: FocusEvent ) => {
if ( ! dropdown.contains( e.relatedTarget as Node ) ) {
dropdown.classList.remove( 'is-active' );
}
} );
trigger.addEventListener( 'keydown', ( e: KeyboardEvent ) => {
if ( e.key === 'Enter' || e.key === ' ' ) {
e.preventDefault();
dropdown.classList.toggle( 'is-active' );
}
} );
} );
// Sticky navbar scroll class.
const navbar = document.querySelector<HTMLElement>( '.site-navbar' );
if ( navbar ) {
const onScroll = (): void => {
navbar.classList.toggle( 'is-scrolled', window.scrollY > 10 );
};
window.addEventListener( 'scroll', onScroll, { passive: true } );
onScroll();
}
// Navbar search toggle.
const searchToggle = document.querySelector<HTMLButtonElement>( '.navbar-search-toggle' );
const searchForm = document.getElementById( 'navbar-search-form' );
if ( searchToggle && searchForm ) {
searchToggle.addEventListener( 'click', () => {
const isHidden = searchForm.hidden;
searchForm.hidden = ! isHidden;
searchToggle.setAttribute( 'aria-expanded', String( isHidden ) );
if ( isHidden ) {
const input = searchForm.querySelector<HTMLInputElement>( 'input' );
input?.focus();
}
} );
// Close when clicking outside.
document.addEventListener( 'click', ( e: MouseEvent ) => {
const target = e.target as Node;
if ( ! searchToggle.contains( target ) && ! searchForm.contains( target ) ) {
searchForm.hidden = true;
searchToggle.setAttribute( 'aria-expanded', 'false' );
}
} );
}
}
+26
View File
@@ -0,0 +1,26 @@
/**
* reading-progress.ts
* Fills a thin progress bar at the top of single-post pages
* based on how far down the user has scrolled.
*/
export function initReadingProgress(): void {
const progressEl = document.querySelector<HTMLElement>( '.reading-progress' );
const barEl = document.querySelector<HTMLElement>( '.reading-progress__bar' );
if ( ! progressEl || ! barEl ) return;
const article = document.querySelector<HTMLElement>( '.single-article' ) ?? document.body;
const update = (): void => {
const articleTop = article.getBoundingClientRect().top + window.scrollY;
const articleHeight = article.offsetHeight;
const scrolled = window.scrollY - articleTop;
const progress = Math.min( 100, Math.max( 0, ( scrolled / articleHeight ) * 100 ) );
barEl.style.width = `${ progress }%`;
progressEl.setAttribute( 'aria-valuenow', String( Math.round( progress ) ) );
};
window.addEventListener( 'scroll', update, { passive: true } );
update();
}
+45
View File
@@ -0,0 +1,45 @@
/**
* theme-toggle.ts
* Dark-mode toggle with localStorage persistence + system preference detection.
*/
type Theme = 'light' | 'dark';
const STORAGE_KEY = 'cyywordpress-theme';
function getSystemTheme(): Theme {
return window.matchMedia( '(prefers-color-scheme: dark)' ).matches ? 'dark' : 'light';
}
function applyTheme( theme: Theme ): void {
document.documentElement.setAttribute( 'data-theme', theme );
}
function getSavedTheme(): Theme | null {
const saved = localStorage.getItem( STORAGE_KEY );
return saved === 'dark' || saved === 'light' ? saved : null;
}
export function initThemeToggle(): void {
// Apply saved or system theme immediately (before paint).
const initialTheme = getSavedTheme() ?? getSystemTheme();
applyTheme( initialTheme );
const btn = document.getElementById( 'theme-toggle' ) as HTMLButtonElement | null;
if ( ! btn ) return;
btn.addEventListener( 'click', () => {
const current = document.documentElement.getAttribute( 'data-theme' ) as Theme | null;
const next: Theme = current === 'dark' ? 'light' : 'dark';
applyTheme( next );
localStorage.setItem( STORAGE_KEY, next );
btn.setAttribute( 'aria-label', next === 'dark' ? '切换浅色模式' : '切换深色模式' );
} );
// Respond to OS-level change when user hasn't set a preference.
window.matchMedia( '(prefers-color-scheme: dark)' ).addEventListener( 'change', ( e ) => {
if ( getSavedTheme() === null ) {
applyTheme( e.matches ? 'dark' : 'light' );
}
} );
}
+104
View File
@@ -0,0 +1,104 @@
/**
* toc.ts
* Automatically generates a Table of Contents from h2/h3 headings
* in the article body and highlights the active heading while scrolling.
*/
interface TocItem {
id: string;
text: string;
level: 2 | 3;
}
export function initToc(): void {
const tocList = document.getElementById( 'toc-list' );
const article = document.querySelector<HTMLElement>( '.single-article .entry-content, .single-article .post-card__content' );
if ( ! tocList || ! article ) return;
const headings = Array.from(
article.querySelectorAll<HTMLHeadingElement>( 'h2, h3' )
);
if ( headings.length < 2 ) {
// Hide TOC widget when there aren't enough headings.
const tocWidget = document.getElementById( 'toc' );
if ( tocWidget ) tocWidget.hidden = true;
return;
}
const items: TocItem[] = headings.map( ( heading ) => {
if ( ! heading.id ) {
// Generate a slug from heading text.
heading.id = heading.textContent
?.trim()
.toLowerCase()
.replace( /[^\w\u4e00-\u9fa5\s-]/g, '' )
.replace( /\s+/g, '-' )
.slice( 0, 60 ) ?? `heading-${ Math.random().toString( 36 ).slice( 2 ) }`;
}
return {
id: heading.id,
text: heading.textContent?.trim() ?? '',
level: heading.tagName === 'H2' ? 2 : 3,
};
} );
// Build ordered list HTML.
const ol = document.createElement( 'ol' );
let currentUl: HTMLUListElement | null = null;
items.forEach( ( item ) => {
const li = document.createElement( 'li' );
const link = document.createElement( 'a' );
link.href = `#${ item.id }`;
link.textContent = item.text;
link.className = `toc-link toc-level-${ item.level }`;
link.addEventListener( 'click', ( e ) => {
e.preventDefault();
const target = document.getElementById( item.id );
target?.scrollIntoView( { behavior: 'smooth', block: 'start' } );
} );
li.appendChild( link );
if ( item.level === 2 ) {
currentUl = null;
ol.appendChild( li );
} else {
// Nest h3 items under the parent h2 list item.
if ( ! currentUl ) {
currentUl = document.createElement( 'ul' );
currentUl.className = 'toc-sub-list';
const lastH2 = ol.querySelector( 'li:last-child' );
lastH2?.appendChild( currentUl );
}
currentUl.appendChild( li );
}
} );
tocList.appendChild( ol );
// Intersection Observer highlight active heading.
const links = tocList.querySelectorAll<HTMLAnchorElement>( '.toc-link' );
const observer = new IntersectionObserver(
( entries ) => {
entries.forEach( ( entry ) => {
const id = entry.target.getAttribute( 'id' ) ?? '';
const link = tocList.querySelector<HTMLAnchorElement>( `a[href="#${ id }"]` );
if ( link ) {
link.classList.toggle( 'toc-active', entry.isIntersecting );
}
} );
},
{
rootMargin: '-80px 0px -60% 0px',
threshold: 0,
}
);
headings.forEach( ( h ) => observer.observe( h ) );
}
+3 -3
View File
@@ -4,9 +4,9 @@ Theme URI: http://underscores.me/
Author: Underscores.me Author: Underscores.me
Author URI: http://underscores.me/ Author URI: http://underscores.me/
Description: Description Description: Description
Version: 1.0.0 Version: 2.0.0
Tested up to: 5.4 Tested up to: 6.7
Requires PHP: 5.6 Requires PHP: 8.1
License: GNU General Public License v2 or later License: GNU General Public License v2 or later
License URI: LICENSE License URI: LICENSE
Text Domain: cyywordpress Text Domain: cyywordpress
+138 -28
View File
@@ -2,50 +2,139 @@
/** /**
* Template part for displaying posts * Template part for displaying posts
* *
* Modernised card layout:
* - 16:9 featured image with lazy-load
* - Category colour badges
* - Relative publish time + hover absolute time
* - Reading-time estimate
* - Animated "Continue reading" button
* - Skeleton loading CSS hook `.card--loading`
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/ * @link https://developer.wordpress.org/themes/basics/template-hierarchy/
* *
* @package CyyWordpress * @package CyyWordpress
*/ */
// Estimate reading time (200 wpm average).
$word_count = str_word_count( wp_strip_all_tags( get_the_content() ) );
$reading_mins = max( 1, (int) ceil( $word_count / 200 ) );
?> ?>
<div id="post-<?php the_ID(); ?>" class="card" style=" margin-bottom: 25px;"> <article id="post-<?php the_ID(); ?>" <?php post_class( 'post-card card' ); ?>>
<div class="card-content">
<div class="media">
<div class="media-content">
<?php if ( has_post_thumbnail() ) : ?>
<div class="post-card__image card-image">
<figure class="image is-16by9">
<?php
the_post_thumbnail(
'card-thumbnail',
array(
'class' => 'post-card__thumb',
'loading' => 'lazy',
'decoding'=> 'async',
'alt' => the_title_attribute( array( 'echo' => false ) ),
)
);
?>
</figure>
</div>
<?php else : ?>
<div class="post-card__image post-card__image--placeholder card-image" aria-hidden="true">
<figure class="image is-16by9">
<div class="post-card__gradient-placeholder">
<?php
$cats = get_the_category();
echo '<span class="post-card__category-initial">'
. esc_html( $cats ? mb_substr( $cats[0]->name, 0, 1 ) : mb_substr( get_bloginfo( 'name' ), 0, 1 ) )
. '</span>';
?>
</div>
</figure>
</div>
<?php endif; ?>
<div class="card-content">
<!-- Category badges -->
<?php
$categories = get_the_category();
if ( $categories ) :
echo '<div class="post-card__categories">';
foreach ( array_slice( $categories, 0, 3 ) as $cat ) {
printf(
'<a class="post-card__category-badge tag is-link is-light" href="%s">%s</a>',
esc_url( get_category_link( $cat->term_id ) ),
esc_html( $cat->name )
);
}
echo '</div>';
endif;
?>
<!-- Title -->
<?php <?php
if ( is_singular() ) : if ( is_singular() ) :
the_title( '<p class="title is-4">', '</p>' ); the_title( '<h1 class="post-card__title title is-4">', '</h1>' );
else : else :
the_title( '<p class="title is-4"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></p>' ); the_title(
'<h2 class="post-card__title title is-4"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">',
'</a></h2>'
);
endif; endif;
?>
if ( 'post' === get_post_type() ) : <!-- Meta -->
?> <?php if ( 'post' === get_post_type() ) : ?>
<p class="subtitle is-6"> <div class="post-card__meta subtitle is-6">
<span class="post-card__date">
<i class="fa-regular fa-calendar" aria-hidden="true"></i>
<time
class="entry-date published"
datetime="<?php echo esc_attr( get_the_date( DATE_W3C ) ); ?>"
title="<?php echo esc_attr( get_the_date() ); ?>"
>
<?php <?php
cyywordpress_posted_on(); echo esc_html(
cyywordpress_posted_by(); sprintf(
/* translators: %s: human-readable time diff */
__( '%s前', 'cyywordpress' ),
human_time_diff( (int) get_the_time( 'U' ), time() )
)
);
?> ?>
</p><!-- .entry-meta --> </time>
</span>
<span class="post-card__author">
<i class="fa-regular fa-user" aria-hidden="true"></i>
<a href="<?php echo esc_url( get_author_posts_url( (int) get_the_author_meta( 'ID' ) ) ); ?>">
<?php echo esc_html( get_the_author() ); ?>
</a>
</span>
<span class="post-card__reading-time">
<i class="fa-regular fa-clock" aria-hidden="true"></i>
<?php
printf(
/* translators: %d: reading time in minutes */
esc_html__( '约 %d 分钟', 'cyywordpress' ),
(int) $reading_mins
);
?>
</span>
</div><!-- .post-card__meta -->
<?php endif; ?> <?php endif; ?>
</div>
</div>
<div class="content"> <!-- Content / excerpt -->
<div class="post-card__content content">
<?php <?php
if ( is_singular() ) :
the_content( the_content(
sprintf( sprintf(
wp_kses( wp_kses(
/* translators: %s: Name of current post. Only visible to screen readers */ /* translators: %s: post title (screen-reader only) */
// __( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'cyywordpress' ), __( '继续阅读<span class="screen-reader-text"> "%s"</span>', 'cyywordpress' ),
__( '继续阅读 <span class="screen-reader-text"> "%s"</span>', 'cyywordpress' ), array( 'span' => array( 'class' => array() ) )
array(
'span' => array(
'class' => array(),
),
)
), ),
wp_kses_post( get_the_title() ) wp_kses_post( get_the_title() )
) )
@@ -53,15 +142,36 @@
wp_link_pages( wp_link_pages(
array( array(
'before' => '<div class="page-links">' . esc_html__( 'Pages:', 'cyywordpress' ), 'before' => '<div class="page-links">' . esc_html__( '页面:', 'cyywordpress' ),
'after' => '</div>', 'after' => '</div>',
) )
); );
else :
the_excerpt();
endif;
?> ?>
</div><!-- .post-card__content -->
<!-- Footer: tags + read-more -->
<?php if ( ! is_singular() ) : ?>
<div class="post-card__footer">
<?php cyywordpress_entry_footer(); ?>
<a
class="post-card__read-more button is-link is-light is-small"
href="<?php echo esc_url( get_permalink() ); ?>"
rel="bookmark"
aria-label="<?php echo esc_attr( sprintf( __( '继续阅读 %s', 'cyywordpress' ), get_the_title() ) ); ?>"
>
<?php esc_html_e( '继续阅读', 'cyywordpress' ); ?>
<span class="post-card__read-more-arrow" aria-hidden="true">&nbsp;&rarr;</span>
</a>
</div>
<?php else : ?>
<footer class="entry-footer"> <footer class="entry-footer">
<?php cyywordpress_entry_footer(); ?> <?php cyywordpress_entry_footer(); ?>
</footer><!-- .entry-footer --> </footer>
</div> <?php endif; ?>
</div>
</div> </div><!-- .card-content -->
</article><!-- #post-<?php the_ID(); ?> -->
+127
View File
@@ -0,0 +1,127 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": {
"defaultPalette": false,
"palette": [
{ "name": "Primary", "slug": "primary", "color": "#00d1b2" },
{ "name": "Primary Dark", "slug": "primary-dark", "color": "#00a896" },
{ "name": "Dark", "slug": "dark", "color": "#363636" },
{ "name": "Light", "slug": "light", "color": "#f5f5f5" },
{ "name": "White", "slug": "white", "color": "#ffffff" },
{ "name": "Danger", "slug": "danger", "color": "#f14668" },
{ "name": "Warning", "slug": "warning", "color": "#ffe08a" },
{ "name": "Info", "slug": "info", "color": "#3e8ed0" },
{ "name": "Success", "slug": "success", "color": "#48c78e" }
]
},
"typography": {
"fontFamilies": [
{
"name": "Noto Sans SC",
"slug": "noto-sans-sc",
"fontFamily": "'Noto Sans SC', sans-serif",
"fontFace": [
{
"src": [ "file:./assets/fonts/NotoSansSC-Regular.woff2" ],
"fontWeight": "400",
"fontStyle": "normal",
"fontDisplay": "swap"
},
{
"src": [ "file:./assets/fonts/NotoSansSC-Bold.woff2" ],
"fontWeight": "700",
"fontStyle": "normal",
"fontDisplay": "swap"
}
]
},
{
"name": "System UI",
"slug": "system-ui",
"fontFamily": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
}
],
"fontSizes": [
{ "name": "Small", "slug": "small", "size": "0.875rem", "fluid": false },
{ "name": "Normal", "slug": "normal", "size": "1rem", "fluid": false },
{ "name": "Medium", "slug": "medium", "size": "1.25rem", "fluid": false },
{ "name": "Large", "slug": "large", "size": "1.5rem", "fluid": { "min": "1.25rem", "max": "1.5rem" } },
{ "name": "X-Large", "slug": "x-large", "size": "2rem", "fluid": { "min": "1.5rem", "max": "2rem" } },
{ "name": "XX-Large","slug": "xx-large","size": "2.5rem", "fluid": { "min": "1.75rem", "max": "2.5rem" } }
],
"lineHeight": true,
"letterSpacing": true
},
"layout": {
"contentSize": "760px",
"wideSize": "1200px"
},
"spacing": {
"spacingScale": {
"operator": "*",
"increment": 1.5,
"steps": 7,
"mediumStep": 1.5,
"unit": "rem"
},
"spacingSizes": [
{ "name": "1", "slug": "10", "size": "0.25rem" },
{ "name": "2", "slug": "20", "size": "0.5rem" },
{ "name": "3", "slug": "30", "size": "1rem" },
{ "name": "4", "slug": "40", "size": "1.5rem" },
{ "name": "5", "slug": "50", "size": "2rem" },
{ "name": "6", "slug": "60", "size": "3rem" },
{ "name": "7", "slug": "70", "size": "4.5rem" }
],
"padding": true,
"margin": true,
"blockGap": true
},
"border": {
"color": true,
"radius": true,
"style": true,
"width": true
},
"shadow": {
"defaultPresets": false,
"presets": [
{ "name": "Small", "slug": "sm", "shadow": "0 1px 3px rgba(0,0,0,.1)" },
{ "name": "Medium", "slug": "md", "shadow": "0 4px 6px rgba(0,0,0,.1)" },
{ "name": "Large", "slug": "lg", "shadow": "0 10px 20px rgba(0,0,0,.12)" }
]
},
"custom": {
"colorBackground": "#ffffff",
"colorText": "#363636",
"colorSurface": "#f5f5f5",
"colorBorder": "#dbdbdb"
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--white)",
"text": "var(--wp--preset--color--dark)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--noto-sans-sc)",
"fontSize": "var(--wp--preset--font-size--normal)",
"lineHeight": "1.7"
},
"spacing": {
"blockGap": "var(--wp--preset--spacing--40)"
},
"elements": {
"link": {
"color": { "text": "var(--wp--preset--color--primary)" },
":hover": { "color": { "text": "var(--wp--preset--color--primary-dark)" } }
},
"h1": { "typography": { "fontSize": "var(--wp--preset--font-size--xx-large)", "lineHeight": "1.2" } },
"h2": { "typography": { "fontSize": "var(--wp--preset--font-size--x-large)", "lineHeight": "1.3" } },
"h3": { "typography": { "fontSize": "var(--wp--preset--font-size--large)", "lineHeight": "1.4" } },
"h4": { "typography": { "fontSize": "var(--wp--preset--font-size--medium)", "lineHeight": "1.5" } }
}
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./assets/js",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@scss/*": ["scss/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "vendor", "assets"]
}
+60
View File
@@ -0,0 +1,60 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
/**
* Vite config for CyyWordpress theme.
*
* Outputs pre-hashed assets to assets/js/ and assets/css/.
* Run `npm run dev` → HMR dev server on :3000
* Run `npm run build` → production bundles with contenthash
*/
export default defineConfig( ( { command } ) => ( {
root: resolve( __dirname, 'src' ),
resolve: {
alias: {
'@scss': resolve( __dirname, 'scss' ),
'@': resolve( __dirname, 'src' ),
},
},
css: {
preprocessorOptions: {
scss: {
// Expose abstracts to every SCSS file without manual imports.
additionalData: `@use "@scss/abstracts/variables" as *;
@use "@scss/abstracts/mixins" as *;
`,
},
},
},
build: {
outDir: resolve( __dirname, 'assets' ),
emptyOutDir: false,
manifest: true,
rollupOptions: {
input: {
main: resolve( __dirname, 'src/ts/main.ts' ),
admin: resolve( __dirname, 'src/admin/customizer.ts' ),
},
output: {
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: ( assetInfo ) => {
if ( assetInfo.name?.endsWith( '.css' ) ) {
return 'css/[name].[hash][extname]';
}
return 'img/[name].[hash][extname]';
},
},
},
},
server: {
port: 3000,
// Proxy WordPress requests through the local server.
// Point WP_HOME to http://localhost:3000 in wp-config.php during dev.
cors: true,
},
} ) );