
Preface
This article carries a strong personal tone. If you find it uncomfortable to read, please close it immediately. This article serves solely as a personal learning record. You are welcome to repost or share it within the scope of the license agreement; please respect the copyright and keep the original link. Thank you for your understanding and cooperation. If you find this site helpful, you can subscribe to it via RSS. Thanks for your support!
Background
When writing blog posts, I often need to show code in both Swift and Objective-C, or multiple implementations of the same feature. Previously, I could only stack code blocks one after another, which made for a poor reading experience.
This time I’ve added tabbed code blocks to the blog. Clicking a tab lets readers switch freely between different languages/implementations, similar to modern documentation platforms like VitePress and Docusaurus.
Implementation
I use the jekyll-tabs gem, which offers:
- ✅ No dependency on any JS framework; plays well with the existing jQuery/Bootstrap
- ✅ Supports multiple independent tab groups on the same page
- ✅ Supports syncing tabs with the same label across groups
- ✅ Supports one-click code copying
- ✅ Built and deployed locally, not limited by GitHub Pages plugin restrictions
Installation Steps
Step 1: Add the Dependency to Gemfile
1
2
3
4
group :jekyll_plugins do
# ... other gems
gem "jekyll-tabs"
end
Then run the installation:
1
bundle install
Step 2: Declare the Plugin in _config.yml
1
2
plugins:
- jekyll-tabs
Step 3: Include the JS
In the post section of _includes/js-selector.html, add:
1
2
3
{% elsif page.layout == 'post' %}
<script async src="{{ '/assets/js/post.min.js' | relative_url }}"></script>
<script defer src="{{ '/assets/AISource/jekyll-tabs/tabs.js' | relative_url }}"></script>
JS initialization config (at the end of tabs.js):
1
2
3
4
5
6
7
8
9
10
11
12
13
window.addEventListener('load', function () {
jekyllTabs.init({
syncTabsWithSameLabels: true, // 同名 Tab 跨组联动
activateTabFromUrl: false, // 关闭 URL hash,避免点击跳顶
addCopyToClipboardButtons: true, // 开启复制按钮
copyToClipboardSettings: {
buttonHTML: '<button class="jekyll-tabs-copy-btn" title="Copy to clipboard"><i class="far fa-copy"></i></button>',
showToastMessageOnCopy: true,
toastMessage: '已复制到剪贴板',
toastDuration: 2000,
}
});
});
Step 4: Include the CSS
In _includes/head.html, add:
1
2
<!-- Jekyll Tabs -->
<link rel="stylesheet" href="{{ '/assets/AISource/jekyll-tabs/tabs.css' | relative_url }}">
Resource File Locations
All resources are placed under the assets/AISource/jekyll-tabs/ directory:
1
2
3
4
assets/AISource/
└── jekyll-tabs/
├── tabs.js # 官方 JS + 初始化配置
└── tabs.css # 融合博客主题变量的自定义样式
Usage
Use the following syntax in your post’s Markdown:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% tabs 组名 %}
{% tab 组名 标签名 %}
```语言
// code content
```
{% endtab %}
{% tab 组名 另一个标签名 %}
```语言
// code content
```
{% endtab %}
{% endtabs %}
Note: The group name in
{% tabs 组名 %}and{% tab 组名 标签名 %}must be consistent, and multiple tab groups on the same page should use different group names to tell them apart.
Examples
Example 1: Printing Hello World in Swift vs Objective-C
1 2
let greeting = "Hello, World!" print(greeting)
1 2
NSString *greeting = @"Hello, World!"; NSLog(@"%@", greeting);
Example 2: Singleton Pattern
1 2 3 4 5 6 7 8 9 10 11
class NetworkManager { static let shared = NetworkManager() private init() {} func request(url: String) { print("requesting: \(url)") } } // Usage NetworkManager.shared.request(url: "https://sunyazhou.com")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
@interface NetworkManager : NSObject + (instancetype)sharedManager; - (void)requestWithURL:(NSString *)url; @end @implementation NetworkManager + (instancetype)sharedManager { static NetworkManager *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (void)requestWithURL:(NSString *)url { NSLog(@"requesting: %@", url); } @end // Usage [[NetworkManager sharedManager] requestWithURL:@"https://sunyazhou.com"];
Example 3: GCD Asynchronous Execution
1 2 3 4 5 6 7 8 9
DispatchQueue.global(qos: .background).async { // Run the time-consuming task in the background let result = heavyTask() DispatchQueue.main.async { // Update the UI back on the main thread self.label.text = result } }
1 2 3 4 5 6 7 8 9
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Run the time-consuming task in the background NSString *result = [self heavyTask]; dispatch_async(dispatch_get_main_queue(), ^{ // Update the UI back on the main thread self.label.text = result; }); });
Example 4: Comparing Three Languages
1 2 3 4 5
func greet(name: String) -> String { return "你好, \(name)!" } print(greet(name: "孙亚洲"))
1 2 3 4
def greet(name: str) -> str: return f"你好, {name}!" print(greet("孙亚洲"))
1 2 3 4 5
function greet(name) { return `你好, ${name}!`; } console.log(greet("孙亚洲"));
Summary
The entire integration involved minimal changes, touching only 4 files:
| File | Change |
|---|---|
Gemfile | Add gem "jekyll-tabs" |
_config.yml | Add plugins: - jekyll-tabs |
_includes/head.html | Include tabs.css |
_includes/js-selector.html | Include tabs.js on post pages |
The result works as expected: clicking a tab doesn’t jump to the top of the page, both dark and light themes are automatically supported, and code highlighting is fully compatible with the existing rouge rendering.