เครื่องมือสร้างเว็บไซต์สแตติกด้วย "Make" (2022)
(karl.berlin)- เว็บไซต์สแตติกโฮสต์ได้ง่าย เร็ว และดูแลรักษาน้อย ดังนั้นถ้าเป็นเว็บไซต์เรียบง่าย ก็อาจสร้างได้ด้วย Makefile ไม่กี่บรรทัด โดยไม่ต้องไปเรียนรู้ตัวสร้างเว็บไซต์แยกต่างหาก
- โครงสร้างหลักคือคงการจัดวางไฟล์ใน
sourceไว้ แล้วเติมheader.htmlเข้าไปใน HTML ส่วนไฟล์ที่เหลือก็ คัดลอกตรง ๆ ไปที่build make buildจับคู่ไฟล์นำเข้าและไฟล์ส่งออกด้วยfind source -type fและpatsubstแล้วใช้ กฎของ make คนละแบบกับไฟล์.htmlและไฟล์ประเภทอื่น- การไฮไลต์หน้าปัจจุบัน, การแปลง Markdown, เซิร์ฟเวอร์โลคัล, การรีบิลด์เมื่อมีการเปลี่ยนแปลง, และการดีพลอยไป GitHub Pages สามารถเพิ่มเป็น target เสริม ได้เมื่อจำเป็น
- เพราะมี dependency น้อยและจุดที่ต้องแก้ชัดเจน สำหรับเว็บไซต์ที่มีความต้องการไม่มาก โฟลว์การ build ที่ทำเอง อาจเร็วกว่าเครื่องมือสร้างแบบอเนกประสงค์
พื้นฐานการทำเว็บไซต์สแตติกด้วย Makefile อย่างเดียว
- เครื่องมือสร้างเว็บไซต์สแตติกให้ผลลัพธ์ที่ โฮสต์ได้ง่าย รวดเร็ว และมีภาระในการดูแลรักษาน้อยมาก
- ถ้าเป็นเว็บไซต์เรียบง่าย การเขียนสคริปต์เองอาจทั้งเร็วกว่าและน่าพอใจกว่าการไปเรียนรู้และปรับใช้ตัวสร้างเว็บไซต์ที่มีอยู่
- ถ้าเป็น เว็บไซต์ธรรมดา ที่ไม่มี timestamp อัปเดตอัตโนมัติหรือ RSS feed ก็ใช้โครงสร้างที่ง่ายกว่าสคริปต์ทำบล็อกได้เพียงพอ
-
ข้อกำหนดพื้นฐาน
- เก็บไฟล์นำเข้าทั้งหมดไว้ในไดเรกทอรี
sourceและคง โครงสร้างไดเรกทอรี เดิมไว้ในผลลัพธ์ด้วย - เพิ่ม
header.htmlไว้ด้านหน้าของไฟล์ HTML ทุกไฟล์ - ไฟล์ที่ไม่ใช่ HTML ให้คัดลอกไปยังไดเรกทอรี
buildโดยไม่เปลี่ยนแปลง
- เก็บไฟล์นำเข้าทั้งหมดไว้ในไดเรกทอรี
-
กฎของ Makefile
- target
buildมี dependency ที่จับคู่ไฟล์ทั้งหมดใต้sourceให้เป็นไฟล์ผลลัพธ์ใต้build - แม้ตัวมันเองจะไม่มีงานโดยตรง แต่ทำหน้าที่เป็นจุดเริ่มให้กฎที่เหมาะกับแต่ละไฟล์ถูกนำไปใช้
- กฎ
build/%.htmlขึ้นกับsource/%.html,header.html, และMakefile - ใช้
mkdir -p $(dir $@)เพื่อสร้างไดเรกทอรีผลลัพธ์ และใช้cat header.html $< > $@เพื่อรวม header กับ HTML ต้นฉบับเป็นผลลัพธ์ - กฎ
build/%จะใช้cp $< $@เพื่อ คัดลอกตรง ๆ สำหรับไฟล์ที่ไม่ใช่ HTML - เพียงมี
header.htmlและกฎเหล่านี้ ก็สามารถรันmake buildเพื่อสร้างไดเรกทอรีbuildสำหรับเปิดดูในเครื่องหรืออัปขึ้นเว็บเซิร์ฟเวอร์ได้
- target
ตัวอย่างการขยายความสามารถและ target เสริม
-
แสดงหน้าปัจจุบัน
- ถ้าไฮไลต์หน้าปัจจุบันใน navigation ผู้เข้าชมจะเห็นตำแหน่งของตนในเว็บไซต์ได้ทันที
- ในตัวอย่างจะค้นหาลิงก์ใน navigation แล้วเพิ่มคลาส
current sed -E 's|(href="$(subst source,,$<))|class="current" \1|' header.html | cat - $< > $@- วิธีแทนที่จริงควรปรับให้เข้ากับ โครงสร้างมาร์กอัป ที่ใช้งานอยู่
-
สร้าง HTML จาก Markdown
- ถ้าไม่อยากเขียน HTML ตรง ๆ หรือมีคอนเทนต์เดิมเป็น Markdown ก็สามารถต่อท่อเข้ากับตัวแปลง Markdown-to-HTML ได้
- ตัวอย่างใช้ smu
smu $< | cat header.html - > $@- กฎพื้นฐานยังคงสมมติว่า
build/foo.htmlถูกสร้างจากsource/foo.html - สำหรับไฟล์ Markdown ต้องคงนามสกุล
.htmlไว้เช่นเดิม หรือแก้กฎให้ค้นหาไฟล์.mdเป็นอินพุต
-
พรีวิวในเครื่อง
- บางเว็บไซต์พรีวิวได้ไม่ถูกต้องเมื่อเปิดไฟล์โลคัลจากเบราว์เซอร์โดยตรง ซึ่งสาเหตุที่พบบ่อยคือใช้ ลิงก์แบบ absolute แทน relative
- Python ติดตั้งมาอยู่แล้วในหลายระบบ และมีเว็บเซิร์ฟเวอร์ที่เหมาะกับงานนี้รวมมาให้
- target
serve:python -m http.server -d build
-
รีบิลด์อัตโนมัติเมื่อมีการเปลี่ยนแปลง
- ระหว่างทำเว็บไซต์ การต้อง build ใหม่ด้วยมือทุกครั้งเป็นเรื่องยุ่งยาก
- ถ้าใช้
entrก็สามารถสั่งให้รันmake buildอัตโนมัติเมื่อsource,header.html, หรือMakefileเปลี่ยนแปลงได้ - target
watch:find source header.html Makefile | entr make build - ถ้าไม่อยากพึ่ง dependency เพิ่ม ก็สามารถใช้
inotifywaitได้เช่นกัน
-
ดีพลอยไป GitHub Pages
- ถ้าใช้ GitHub เป็นที่เก็บ repository อยู่แล้ว การโฮสต์ HTML ที่สร้างเสร็จบน GitHub Pages ก็เป็นตัวเลือกที่เป็นธรรมชาติ
- การจัดคำสั่งดีพลอยให้ทำงานโดยไม่ต้องกังวลรายละเอียดของ git อาจยุ่งยากเล็กน้อย แต่จัดการได้ด้วย
git worktree - วิธีนี้อ้างอิงจาก บทความของ Sangsoo Nam
- เพิ่มแบรนช์
gh-pagesเป็น worktree ชื่อpublic_html - คัดลอก
build/*ไปยังpublic_html - รัน
git add --all,git commit -m "Deploy to github pages", และgit push origin gh-pages - สุดท้ายลบ worktree ด้วย
git worktree remove public_html
-
ตัวอย่างใช้งานจริง
- สามารถดูหน้าเว็บที่สร้างด้วยแนวทางนี้ได้ที่ karlb/astridbartel.de
- เครื่องมือสร้างเว็บไซต์สแตติกทั้งชุดอาจเริ่มต้นได้ด้วย หกบรรทัดใน Makefile และสามารถปรับให้เข้ากับความต้องการได้อย่างรวดเร็ว โดยไม่มี dependency แปลก ๆ หรือภาระดูแลรักษามากนัก
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ต่อมาได้เพิ่มฟีเจอร์อย่างข่าวและฟีด RSS, รายการอัตโนมัติของบทความวิจัยและเอกสารประกอบการสอน, รวมถึงรายการหนังสือที่กรองด้วยแท็กได้ ตอนนี้ก็ยังเป็น Makefile อยู่ แต่ตัว Makefile เองกลับเรียบง่ายขึ้นเล็กน้อย และไปเรียกใช้สคริปต์ Bash ที่ใช้ยูทิลิตี xml2 และ 2xml ชั้นดีเพื่อจัดการ HTML แบบทีละบรรทัดแทน โดยส่วนใหญ่ใช้ core utilities อย่าง
grepและsedยังติด git hook ไว้หลายตัวเพื่อเรียก
makeอัตโนมัติเมื่อจำเป็น โดยเฉพาะบนเซิร์ฟเวอร์ระยะไกลที่โฮสต์เว็บไซต์ เมื่อ push ไปยัง repository แล้วเวอร์ชันสาธารณะจะถูก build ใหม่ใช้งานได้ดีมากมาหลายปีแล้ว และประวัติ git ย้อนกลับไปได้ถึงปี 2009 พอดู commit แรก ๆ ก็มี
beccad7 (FIRST_VERSION) Initial commit,d1cc6d7 adding link to Google Reader shared items,6ccfd0c fix typo,d337959 adding link to Identi.ca accountและนี่ก็ผ่านมาจริง ๆ 15 ปีแล้วxml2และ2xmlดูเหมือนเป็น โปรเจกต์ที่ถูกปล่อยทิ้งไว้ อยู่บ้างhttps://github.com/cryptorick/xml2
https://manpages.debian.org/unstable/xml2/2xml.1.en.html
source/แล้ว ไฟล์นั้นก็จะไม่ถูกลบจากbuild/ในโปรเจกต์ของผม การ build เว็บไซต์ทั้งหมดยังเร็วพอ จึงเลือก ลบโฟลเดอร์ build ทั้งหมด ก่อน build ใหม่
https://github.com/jez/jez.github.io/blob/source/Makefile#L1...
แบบนี้ทำให้ incremental build ซึ่งเป็นเหตุผลสำคัญอย่างหนึ่งของการใช้ build system ถูกลดทอนประโยชน์ไปมาก แต่ถ้ารู้ว่าต้องการสร้างหน้าไหนใหม่ ก็ยังสามารถ
makeเฉพาะไฟล์นั้นโดยตรงได้ถ้ามี workaround ที่ใช้กันทั่วไปสำหรับแพตเทิร์นแบบนี้ใน Makefile ก็อยากรู้
ใช้ฟังก์ชัน
shellของ GNU Make เพื่อไล่รายการไฟล์ แล้วใช้ฟังก์ชันfilter-outเพื่อกรอง output ที่ “คาดไว้” ออก เป็นแฮ็กที่ดูไม่สวย แต่ทำให้คำสั่งถูกเรียกเป็นส่วนหนึ่งของการขยายตัวแปรผ่านฟังก์ชันshellจึงรับประกันได้ว่าจะรันทุกครั้งลิงก์ Makefile: https://github.com/jaredkrinke/make-blog/blob/main/Makefile
นอกจากตัวเลือกแบบระเบิดนิวเคลียร์อย่าง
make cleanแล้ว ก็สามารถมี Make target เฉพาะสำหรับการลบและเปลี่ยนชื่อได้ เช่น ทำให้make rm sourcefileหรือmake mv sourcefile newsourcefileจัดการลบ/เปลี่ยนชื่อทั้งไฟล์ต้นฉบับและผลลัพธ์ที่สร้างขึ้นพร้อมกันในทางปฏิบัติ แม้แต่บล็อกหรือโปรเจกต์ออนไลน์ที่ค่อนข้างใหญ่ วงจร
make clean/make allก็มักจะเร็วพอ และมักจำเป็นเมื่อต้องแก้ template หรือองค์ประกอบดีไซน์ของไซต์ ถ้าขนาดใหญ่จนต้องกังวลเรื่องเวลา build ใหม่จริง ๆ ก็ควรใช้ CMS จริงที่จัดการ source ในฐานข้อมูลและสร้างแบบไดนามิกเมื่อไคลเอนต์เข้าถึงมากกว่าmake cleanก็พอไม่ใช่เหรอ?rm/%.html:@rm -f source/%.html build/%.htmlรันด้วย
$ make rm/page.htmlpruneสำหรับจุดประสงค์นี้โดยตรงhttp://neilmitchell.blogspot.com/2015/04/cleaning-stale-file...
แต่ผมคิดว่าวิธีที่ดีที่สุดที่ใช้ได้กับ
makeก็คือมี targetmake distที่สร้าง archive.tar.gzสุดท้ายของผลลัพธ์ ถ้าเขียน rule ถูกต้อง ไฟล์เก่าจะไม่ถูกใส่เข้าไป ในโปรเจกต์ใหญ่ ๆ อาจช้าได้ แต่ตอนพัฒนาก็ไม่จำเป็นต้องใช้ และใช้เฉพาะตอน release ก็พอ incremental build ยังทำได้เหมือนเดิม แค่.tar.gzสุดท้ายเท่านั้นที่ต้องสร้างใหม่ตั้งแต่ต้นblog.shของ Karl ที่กล่าวถึงในบทความนี้ผมนำมันมาปรับแก้จนกลายเป็น
barfซึ่งเป็น static site generator แบบมินิมอลของผมเอง ถ้า Karl ไม่ได้เผยแพร่งานเจ๋ง ๆ ของเขาออกมา มันก็คงไม่มีอยู่[0]: https://github.com/karlb/karl.berlin/blob/master/blog.sh
[1]: https://barf.bt.ht
shiteและใช้มันสร้างเว็บไซต์ของผมเอง ชื่อนี้บอกใบ้ถึงคุณภาพซอฟต์แวร์ :)สิ่งที่ชอบที่สุดคือจนถึงตอนนี้ไม่เคยต้องอัปเกรดอะไรเลย และดูเหมือนอนาคตก็คงไม่ต้องด้วย สิ่งที่ดีรองลงมาคือมี hot reload ได้โดยไม่ใช้ JavaScript
[1] https://github.com/adityaathalye/shite
[2] https://evalapply.org
m4เข้าไปนิดหน่อย ก็จะได้ความยืดหยุ่นเพิ่มขึ้นเล็กน้อยในขณะที่ยังคง แนวทางแบบโครงกระดูกเปล่า ๆ เดิมไว้เคยดูแลเว็บไซต์เล็ก ๆ ที่ทำด้วยวิธีแบบนั้นเมื่อราว 20 ปีก่อน แต่ถ้าไม่นับเว็บไซต์ส่วนตัว ก็ไม่แน่ใจว่าทุกวันนี้โมเดลนี้จะเหมาะแค่ไหน เพราะโดยเนื้อแท้แล้ววิธีนี้บังคับให้กลับไปสู่บทบาทแบบ Web 1.0 ผู้ใช้ทุกคนที่มีส่วนร่วมต้องเชี่ยวชาญ HTML หรือไม่ก็ต้องมีใครสักคนรับงานจิปาถะในฐานะ “เว็บมาสเตอร์”
[1] https://en.wikipedia.org/wiki/M4_(computer_language)
m4นิดหน่อย”เริ่มโปรเจกต์สะอาด ๆ พร้อมสาบานว่าคราวนี้จะไม่แตะ
m4แล้วก็ใส่การเรียกm4เล็ก ๆ อันหนึ่งเข้าไปเพื่อลดโค้ดซ้ำหนึ่งปีต่อมา ต้องขุดลงไปใน macro expansion 5 ชั้นเพื่อหาว่าทำไมคำว่า “cat” ถึงหายไปจากเว็บไซต์อย่างเงียบ ๆ และสุดท้ายก็พบว่านักพัฒนารุ่นจูเนียร์พยายามเขียนลูป for เองแทนที่จะคัดลอกจากคู่มือ แล้วทำเครื่องหมายคำพูดพัง
พอแก้ปัญหาเฉพาะหน้าเสร็จ ก็สรุปว่า DSL ดีบักยากเกินไป เลยเอาไฟล์แมโคร M4 ที่คัดลอกข้ามโปรเจกต์กันมาตลอดเข้ามาใช้ จากนั้นใช้เวลาทั้งวันเปลี่ยนตำแหน่งที่ใช้
defineทั้งหมดให้เป็นแมโครที่สร้างแมโคร เพื่อเพิ่มคอมเมนต์ในเอาต์พุตให้สคริปต์สร้าง stack trace ทำงานได้โปรเจกต์หน้าจะตั้งกฎเด็ดขาดเลยว่า ห้าม
m4! แน่นอน อาจมีข้อยกเว้นสักจุดหนึ่งก็ได้m4หรือperlแนะนำให้ใช้ SGML ซึ่งเป็นรากฐานและ superset ร่วมของ HTML กับ XMLSGML มีแมโครข้อความแบบตรวจชนิดง่าย ๆ หรือก็คือ entity expansion มาให้ในตัว และยังทำ parameterized macro expansion ที่รู้ชนิดได้ด้วย ในที่นี้ชนิดหมายถึงชนิดเนื้อหาทั่วไปขององค์ประกอบมาร์กอัป หรือก็คือลูกที่อนุญาตและลำดับของมัน รวมถึงพิจารณาการขยายหรือ escape ตามบริบทอย่าง attribute, CDATA, RCDATA ด้วย
งานอย่างการขยายและ escape ความคิดเห็นผู้ใช้ที่อาจเป็นอันตรายอย่างเหมาะสม พร้อมบังคับใช้กฎของผู้ใช้ เช่น อนุญาตมาร์กอัประดับ inline แต่ห้ามองค์ประกอบ
scriptมีเพียง SGML เท่านั้นที่ขยายได้ถูกต้อง นอกจากนี้ยังทำได้ทั้งการขยาย Markdown หรือไวยากรณ์วิกิเป็น HTML, นำเข้าเนื้อหา HTML ภายนอก/สำหรับ syndication, สร้าง RSS และ outline สำหรับการนำทาง เหมาะกับงานเตรียม static site ที่ค่อนข้างซับซ้อนบน command line[1]: https://sgmljs.net/docs/producing-html-tutorial/producing-ht...
[2]: https://sgmljs.net/docs/sgmlproc-manual.html
envsubstแทนm4หรือการ find-and-replace ด้วยsedก็น่าจะดีเป็นโปรแกรมที่แทนที่การอ้างอิงตัวแปรแบบ Bash เช่น
$TITLEด้วยค่าจาก environment variableexport CURRENT="..."cat page.html | envsubtm4นิดหน่อย” น่ะเหรอ ขอร้อง อย่าเด็ดขาดm4ยังห่วยแม้ในฐานะภาษาโปรแกรมแนวลึกลับอ่านยากแค่ข้อเท็จจริงว่ามันเคยรันใน sendmail ได้ ไม่เพียงพอจะเป็นเหตุผลรับรองอะไรทั้งนั้น
ทุกครั้งที่อ่านบทความน่าสนใจที่นี่ก็จะ subscribe ฟีดไว้ ไม่ว่าจะเป็นไซต์ Wordpress, Bear Blog, Micro.blog, บล็อก Havenweb หรือฟีดจากไซต์ที่ทำเอง ก็เพิ่มเข้าไปในโมดูล
Really Social Sitesของ Hey Homepageสุดท้ายแล้วอยากเปิดเผยรายชื่อบล็อกเหล่านี้เหมือนที่ Kagi ทำในโครงการ Small Web แต่ถ้าจะเพิ่มคุณภาพ การคัดสรรคือหัวใจสำคัญ และพอคิดเรื่องการคัดสรรแล้ว การเริ่มนิตยสารออนไลน์สักอย่างก็ดูเป็นขั้นตอนที่เป็นธรรมชาติ
ผู้คนได้ “sense of entitlement” ในความหมายดี ๆ ว่าสิ่งนี้ควรแชร์ให้คนอื่นได้ จากที่ไหนกัน? ผมสงสัยว่าทำไมถึงสมมติว่าคนอื่นจะสนใจสิ่งที่ผมกำลังทำ บางครั้งมันรู้สึกเหมือนการแข่งขัน ประมาณว่า “ถ้าอยากได้ไลก์หรือยอดเห็นบนบล็อก ก็ต้องสร้างอะไรที่เจ๋งที่สุดเท่าที่จะทำได้”
การร่วมมือนั้นดีแน่นอน และจะทำได้ก็ต้องเปิดเผยทั้งหมด เพียงแต่ผมไม่ค่อยเห็นเส้นแบ่งระหว่าง “ทำเพราะสิ่งนี้ดูเจ๋ง” กับ “พยายามแชร์ให้คนอื่นเพื่อให้ได้ปฏิกิริยาตอบกลับ”
เขาบอกว่าแค่เปลี่ยนไฟล์ทดสอบหนึ่งไฟล์ ก็สามารถสร้างบทความทั้งฉบับใหม่ได้ด้วยคำสั่งเดียว รวมถึงการรันทดสอบและสร้างกราฟด้วย
https://pages.github.com/
ของผมออกแบบมากไปหน่อยเพราะอยากได้ hot reload แบบไม่มี JavaScript และเป็นการโกนขนจามรีที่สนุกดี
ไอเดียพื้นฐานเหมือนกัน ใช้ heredoc ในเทมเพลต ใช้คอมไพเลอร์แปลงข้อความธรรมดาเป็น HTML ซึ่งในกรณีของผมคือ
pandocใช้ CSV กลางเพื่อสร้างดัชนี และยังมีเทคนิคsedที่มีประโยชน์สำหรับดึง front matter ออกมา คลาสสิกและดี[1] https://github.com/karlb/karl.berlin/blob/master/blog.sh
[2] https://github.com/adityaathalye/shite
[3] วิธีที่ผมทำ: https://github.com/adityaathalye/shite/blob/master/bin/templ...
แต่ก็มีข้อจำกัดอยู่บ้าง ผมจัดระเบียบบทความตามเนมสเปซและใส่วันที่ใน URL แต่
makeจัดการเรื่องนี้เองได้ไม่ค่อยดีmakeคือเมื่อมีการเปลี่ยนแปลงเล็กน้อยในโปรแกรมขนาดใหญ่ที่ต้อง build ด้วยคอมไพเลอร์ที่ช้า มันช่วยให้การ rebuild แบบ incremental เร็วขึ้นมากงานที่ถ้า rebuild ทั้งหมดต้องใช้เวลา 40 นาที อาจเสร็จในราว 3 วินาที
ถ้า static site ถูกสร้างขึ้นใหม่ตั้งแต่ต้นภายในไม่ถึง 1 วินาที เพียงแค่
catเฮดเดอร์ร่วมกับไฟล์ HTML หลายร้อยไฟล์ ก็ไม่มีประโยชน์อะไรที่จะใช้makeแทนสคริปต์ มีแต่จะเพิ่มความเสี่ยงที่จะ build ได้ไม่สมบูรณ์เพราะบั๊กด้าน dependency.phonyได้ถึงอย่างนั้นก็ยังแยกคำสั่งอย่าง
make buildกับmake pushได้ในโปรเจกต์เล็ก ๆ อื่น ผมใช้ shell script ขนาดจิ๋วเป็น bundler แบบชั่วคราว โดยเป็นวิธีแทรก CSS และ JS เข้าไปใน HTML และมีเป้าหมายให้สามารถ serve ไฟล์ที่ยังไม่ได้ build ในเครื่องได้ด้วย