Đánh thức cỗ máy Lisp: Hành trình xây dựng Static Site Generator thuần túy với Emacs

19 tháng 4, 2026·8 phút đọc

Chán ngấy với sự cồng kềnh của các framework web hiện đại, tác giả đã quyết định tự xây dựng một trình tạo trang tĩnh (SSG) hoàn toàn mới dựa trên sức mạnh của Emacs Lisp và Org-mode, không phụ thuộc vào bất kỳ thư viện bên ngoài nào.

Đánh thức cỗ máy Lisp: Hành trình xây dựng Static Site Generator thuần túy với Emacs

Mạng lưới web hiện đại đang tự làm mình ngột ngạt. Đã có lúc nào đó, chúng ta đánh đổi sự thanh lịch của văn bản thuần (plain text) lấy hàng gigabyte các thư viện node_modules/, những framework JavaScript labyrinthine, và các trình tạo trang tĩnh (Static Site Generators - SSG) cồng kềnh bắt bạn phải học ngôn ngữ template kỳ lạ chỉ để viết một bài đăng blog. Tệ hơn, một số công cụ còn ép bạn phải dùng chuột. Thật kinh khủng.

Tôi không muốn một framework nào nữa. Tôi từ chối tích trữ các phụ thuộc như một người chuẩn bị cho ngày tận thế. Tôi muốn sự thoải mái của trình soạn thảo văn bản yêu thích của mình. Cụ thể là Emacs. Đừng gọi nó là trình soạn thảo văn bản; đó là một cỗ máy Lisp đơn luồng ngụy trang dưới vỏ bọc đó. Bên trong cỗ máy đó là Org-mode. Đây không phải là một bản sao Markdown rẻ tiền dressed up; nó là một mô hình cấu trúc có thể uốn cong theo logic phức tạp một cách dễ dàng. Sắp xếp cuộc sống của bạn bằng Org-mode là điều cơ bản, không phải là sự cường điệu. Những người điên rồ tuyệt đối dùng nó để quản lý tài chính, bảng tính và nắm giữ sự thực mong manh của họ.

Tôi đã đắm chìm trong Org-mode, quản lý các lịch trình khổng lồ và hệ thống ghi chú kiểu Zettelkasten lồng nhau. Việc uốn nắn quy trình làm việc hoặc tổ chức lại ghi chú để chiều lòng cấu trúc thư mục cứng nhắc của một SSG nào đó là điều không thể. Tôi chỉ muốn render suy nghĩ của mình thành HTML thôi.

Vòng đời file trong quy trình xây dựngVòng đời file trong quy trình xây dựng

Tôi đã từng để một dòng ghi chú trong README suốt 5 năm: "Có lẽ tôi sẽ tạo một trang web tĩnh từ những ghi chú này vào một ngày nào đó, không biết khi nào."

Đã đến lúc gọi cược. Mục tiêu rất đơn giản, có lẽ là nguy hiểm: xuất bản các ghi chú viết bằng Org-mode của tôi với số phụ thuộc bên ngoài bằng không.

Sự cám dỗ và thất bại của org-publish

Giống như nhiều người đi trước, tôi đã vấp vào sức hút của org-publish. Việc có một giải pháp xuất bản tích hợp sẵn trong Emacs rất hấp dẫn. Tôi dành hàng giờ để tinh chỉnh, chăm sóc org-publish-projects-alist, chỉ để đập mặt vào thực tế tàn khốc của API mong manh của nó. Mặc dù hứa hẹn khả năng mở rộng vô hạn, nhưng động cơ xuất bản lại cảm giác thô sơ một cách đau đớn. Mã của tôi biến thành một khối hỗn độn của các móc (hooks) dễ vỡ, để tôi cách xa đầu ra HTML mà tôi mong muốn.

Tôi đếm không xuể các trận chiến chống lại hàm template, các URL bị hỏng và cái bản đồ trang (sitemap) đáng chết. Việc xây dựng một chỉ mục phân trang là một hành động vô vọng, cảm giác ít giống lập trình hơn là đàm phán giải cứu con tin với một bức tường gạch. Khả năng mở rộng là một huyền thoại; nó là rùa (turtles) tất cả các đường xuống.

Tìm kiếm giải pháp thay thế

Có lẽ "không phụ thuộc gì cả" là một lời thề tự sát. Tôi đánh giá lại các lựa chọn, tìm kiếm thứ gì đó rides natively trên tính khả kết hợp của Emacs:

  • Worg? Một wiki với script xuất bản đẹp nhưng không phải là generator. Bỏ qua.
  • Hugo+ox-hugo? Ép Org đi qua proxy Markdown đánh bại hoàn toàn mục đích. Sự báng bổ.
  • org-publish? Chúng ta đã xác định là không khả thi.
  • blorgit? Không được chạm vào trong 14 năm. Không thể xử lý sự cồng kềnh của Ruby hay khảo cổ học.
  • jorge? Viết bằng Go. Bỏ qua quá trình xuất bản Emacs gốc. Tiếp theo.
  • Weblorg? … Hmm. Cái này trông thú vị đấy.

Weblorg ticked gần hết mọi ô. Không thiên kiến. Khả năng kết hợp. Chỉ đúng vibe tôi đang săn lùng. Tuy nhiên, nó có ma sát khó chịu của một viên sỏi trong giày; sự phụ thuộc vào string templating làm tôi khó chịu. Tại sao phải phát minh lại một bánh xe vuông kiểu jinja khi tôi đã có cỗ máy Lisp tối thượng rên rỉ dưới đầu ngón tay? Tôi không muốn một động cơ templating khác. Tôi muốn Elisp thuần túy.

Vì vậy, tôi đã loại bỏ các sự thỏa hiệp. Tôi đào lên cái xác thối rữa của bao bọc org-publish thất bại của mình, opd (dumb org-publish distribution), và quyết định tự kỹ thuật đường thoát của mình. Weblorg có khung xương kiến trúc hoàn hảo, nhưng nội tạng của nó yếu. Một ca ghép tạng bạo lực là cần thiết.

Chuyển dịch từ String sang Lisp

Mọi SSG đều bắt đầu với cùng một giả định: "Tôi chỉ cần đọc file, hoán đổi biến, và viết HTML."

Các nguyên mẫu đầu tiên của tôi bẩn thỉu. Tôi bắt đầu bằng cách xé lõi Weblorg ra và thay thế bằng một đường ống thay thế chuỗi tự chế. Nó hoạt động… cho đến khi không. Kiến trúc vỡ vụn ngay khi tôi đi chệch khỏi con đường mòn. Chuỗi thì ngốc nghếch; chúng không có ngữ cảnh.

Tôi đã thực hiện bước chuyển đổi (il)logic tiếp theo: thay thế chuỗi tĩnh bằng các closure được đánh giá động. Tôi cố gắng buôn lậu việc thực thi Lisp động vào một đường ống văn bản phẳng. Đó là một thất bại ngoạn mục; tôi đang cố gắng xây dựng một trình định dạng chuỗi mù quáng bên trong một trình soạn thảo đã sở hữu một trong những trình phân tích cú pháp tinh vi nhất.

Giải pháp thực sự đã ngồi ngay dưới mũi tôi cả thời gian: Tôi không nên đụng vào cái HTML chết tiệt đó.

Tôi ném các template chuỗi vào lửa. Thay vì truyền biến cho một chuỗi ma thuật, tôi trang bị cho các tuyến đường (routes) một tham số :template mong đợi một closure Lisp thuần túy thay vì đường dẫn file hoặc chuỗi. Mặc định :template sụp đổ từ một bộ xương HTML khổng lồ thành một thứ tối giản:

(lambda (ctx)
  (when-let ((path (alist-get 'abspath ctx)))
    (insert-file-contents path)))

Điều này mở khóa một chiều kích của khả năng mở rộng. Tôi có thể lập trình điền vào bộ đệm tạm với bất cứ thứ gì tôi muốn chỉ bằng Lisp.

Trình biên dịch hai lượt (Two-pass Compiler)

Tôi có đầu ra HTML, nhưng các trang cô lập không tạo thành một trang web. Tôi cần một mạng lưới. Kết nối đường dẫn thủ công là việc cho kẻ kém cỏi. Tôi muốn thả một liên kết Org chuẩn [[file:somewhere-far-away/some-file.org]] vào bài viết, để Emacs xử lý nó một cách tự nhiên khi tôi chỉnh sửa, và tin tưởng động cơ sẽ tạo ra permalink chính xác trong quá trình build.

Để dệt nên mạng lưới đó qua mê cung của các liên kết chéo giữa các tuyến đường ảo khác biệt, tôi cần bản đồ. Tôi cần mượn khái niệm "trình biên dịch hai lượt".

  • Lượt 1 (Pass 1): Nhiệm vụ trinh sát. Nó khám phá các file, đánh giá đích đến của chúng, và gieo một :registry URL chính được chia sẻ ở cấp độ site.
  • Lượt 2 (Pass 2): Thực thi render. Tôi chèn một bộ chuyển đổi liên kết tùy chỉnh vào backend dẫn xuất của mình; một điệp viên ngủ (sleeper agent) chặn chặn việc giải quyết liên kết gốc của Org trước khi nó hành động.

Bằng cách liên kết động default-directory bên trong bộ đệm template tạm ngay trước khi gọi org-export-as, tôi đã đánh lừa Emacs giải quyết đường dẫn tương đối một cách tự nhiên. Bộ chuyển đổi chỉ cần truy vấn registry, hoán đổi liên kết, và lùi lại vào bóng tối.

Cô lập trạng thái và Tối ưu hóa hiệu suất

Khi bảng định tuyến mở rộng, một kẻ phản diện mới xuất hiện: State bleeding (rò rỉ trạng thái). Emacs được xây dựng trên nền tảng của các biến toàn cầu. Xử lý Mục lục (Table of Contents) trên blog chính cùng với nguồn RSS không có nó cần sự khéo léo. Giải pháp nằm sâu trong mã nguồn Emacs: một macro Common Lisp cổ xưa gọi là cl-progv. Bằng cách giới thiệu thuộc tính :env vào tuyến đường, tôi có thể ra lệnh cho động cơ liên kết và giải绑 trạng thái thực thi một cách nghiêm ngặt trên cơ sở mỗi tuyến đường.

Về hiệu suất, tôi đã loại bỏ các lambda bao bọc và thay thế bằng cl-loop để loại bỏ chi phí của closure. Tôi cũng sử dụng profiler-start của Emacs để tìm ra các hàm đang gặm nhấm CPU. Kết quả là một động cơ cực nhanh, có thể xử lý hàng ngàn bài viết trong tích tắc.

Kết luận

Hành trình này không chỉ là viết code; đó là một cuộc thập tự chánh để tìm lại sự thanh giản trong một thế giới web quá phức tạp. Bằng cách chấp nhận di sản của Emacs và sức mạnh của Lisp, tôi đã tạo ra một công cụ không chỉ nhanh hơn và gọn nhẹ hơn, mà còn mở rộng vô hạn theo cách mà các framework hiện đại không thể sánh kịp. Đôi khi, giải pháp tốt nhất là xây dựng lại từ đầu bằng những công cụ cũ kỹ nhưng đáng tin cậy nhất.

Đôi khi, quyết định kỹ thuật thông minh nhất là nhận ra khi nào một vấn đề đã được giải quyết từ nửa thế kỷ trước bởi một lệnh shell: rm -rf output/. Tàn bạo, không trạng thái, và chính xác.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗