主页 Adding Tabbed Code Blocks to Your Jekyll Blog
Post
Cancel

Adding Tabbed Code Blocks to Your Jekyll Blog

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:

FileChange
GemfileAdd gem "jekyll-tabs"
_config.ymlAdd plugins: - jekyll-tabs
_includes/head.htmlInclude tabs.css
_includes/js-selector.htmlInclude 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.

该博客文章由作者通过 CC BY 4.0 进行授权。