ทำธีมเองให้ FoliHub
ทุกหน้า public ของ FoliHub มี hook CSS ชื่อ .fh-* ให้ใช้แบบคงที่ — เปิด editor วาง CSS ลงไป สี layout animation SVG ฝังในไฟล์ ได้หมด กดบันทึก แล้วเห็นผลทันที หน้านี้คือ reference ตัวเต็ม — มีครบทุก selector ทุกกฎ sanitizer และตัวอย่างพร้อมใช้ที่บันทึกผ่านตั้งแต่ครั้งแรก
เริ่มต้น
Custom CSS เป็นฟีเจอร์ของแพ็กเกจ Pro (Pro และ Pro+ ใช้ได้ทั้งคู่) พออัปเป็นแพ็กเกจจ่ายเงินแล้ว:
- เปิด /dashboard/customize จะเห็น CodeMirror editor อยู่ซ้าย และ iframe พรีวิวสดอยู่ขวา
- พิมพ์ CSS ได้เลย ทุกครั้งที่บันทึก ระบบจะเก็บเวอร์ชันเก่าไว้ 5 ช่อง — ถ้าพังย้อนกลับได้จากคอลัมน์ขวา
- เติม
?nocss=1ต่อท้าย URL หน้า public ของคุณ (เช่นfolihub.com/meawmutcha?nocss=1) เพื่อดูหน้าแบบไม่ใส่ CSS — ใช้เวลาเผลอซ่อนปุ่มจนหาไม่เจอ
DOM ที่เขียนถึง
ทุกหน้าใช้ <body data-theme="..."> เหมือนกัน CSS ของคุณถูกแทรกหลัง theme variable เลย override อะไรก็ได้โดยไม่ต้องใส่ !important
หน้า Linkfolio (/username)
<body data-theme="amber">
<main class="fh-page fh-linkfolio">
<header class="fh-hero">
<img class="fh-avatar" src="..." />
<h1 class="fh-display-name">Meaw Mutcha</h1>
<p class="fh-tagline">ของน่ารักทำมือ</p>
</header>
<ul class="fh-links">
<li><a class="fh-link-button fh-link-primary fh-link-donate" href="..."> ... </a></li>
<li><a class="fh-link-button fh-link-social fh-link-social-instagram" href="..."> ... </a></li>
<li><a class="fh-link-button fh-link-custom fh-link-custom-{handle}" href="..."> ... </a></li>
<!-- Photo card: image-kind block OR link with cover image. Same class. -->
<li><a class="fh-link-button fh-link-custom fh-link-custom-photo" href="..."> ... </a></li>
<li><div class="fh-text-block fh-text-block--header">DIVIDER</div></li>
</ul>
<footer class="fh-footer">© 2026 ...</footer>
</main>
</body>รายการ selector
นี่คือ hook .fh-* ทุกตัวที่หน้า public เปิดให้ใช้ อย่าเขียนถึง class ที่ไม่อยู่ในนี้ — พวกนั้นเป็น internal เปลี่ยนได้ทุกเมื่อโดยไม่บอกล่วงหน้า
Page structure
.fh-pageWrapper on every public page..fh-linkfolioLinkfolio page only..fh-heroLinkfolio hero section (avatar + name + tagline)..fh-avatarProfile avatar / initials circle..fh-display-nameDisplay name heading..fh-taglineTagline text under the name..fh-linksThe grid of link buttons on the linkfolio..fh-footerThe bottom credit line.
Buttons (any kind)
.fh-link-buttonEvery link button (hover, color, radius)..fh-link-primaryAny button with the star/highlight on..fh-link-socialAll social-media buttons (any platform)..fh-link-customCustom links (anything you create yourself)..fh-link-custom-photoPhoto card — applied both to standalone image blocks and to Link rows that have a cover image attached. Full-bleed image with an optional caption overlay..fh-link-youtubeYouTube preview cards..fh-link-email"Email me" button — auto-shown when a public email is set..fh-link-donateDonate / Support button (when enabled).
Inside a button (icon / label / image / caption)
.fh-link-iconThe icon swatch on the left of every standard button (link / social / custom / email / donate). Photo + YouTube cards do not use it..fh-link-labelThe text label inside every standard button. Use to retypograph button copy without touching the surrounding chrome..fh-link-imageThe <img> element inside a photo card (kind=image OR a link with cover). Style filters / borders / hover scale here..fh-link-captionThe optional caption overlay inside a photo card OR YouTube card — appears only when the user supplied a label.
Per-platform socials
.fh-link-social-lineLINE button..fh-link-social-facebookFacebook button..fh-link-social-instagramInstagram button..fh-link-social-tiktokTikTok button..fh-link-social-youtubeYouTube button..fh-link-social-twitterX / Twitter button..fh-link-social-threadsThreads button..fh-link-social-websiteWebsite button..fh-link-social-githubGitHub button..fh-link-social-linkedinLinkedIn button..fh-link-social-discordDiscord button..fh-link-social-behanceBehance button..fh-link-social-dribbbleDribbble button..fh-link-social-figmaFigma button..fh-link-social-mediumMedium button..fh-link-social-substackSubstack button..fh-link-social-producthuntProduct Hunt button..fh-link-social-shopShop button.
Targeting your own custom links
.fh-link-custom-{handle}Set a CSS handle on any custom link (link editor → Edit → CSS handle) to target it directly..fh-text-blockAny text block between buttons..fh-text-block--headerShort uppercase divider..fh-text-block--paragraphLonger prose text block.
กฎ sanitizer
CSS จะถูกตรวจตอนบันทึก อะไรที่นอกเหนือกฎพวกนี้ถูกปัดทิ้ง พร้อม error ที่บอกชัดว่าผิดบรรทัดไหน
ใช้ได้
- property CSS มาตรฐาน, pseudo-class, pseudo-element, media query,
@keyframesanimation ใช้ได้หมด - หน่วย:
px rem em % vw vh svh dvh+ ฟังก์ชันclamp() min() max() calc() - custom property (CSS variable), transform, transition, filter, gradient,
backdrop-filter - URL ใน
@importอนุญาตเฉพาะ:fonts.googleapis.com,fonts.gstatic.com,fonts.bunny.net,cdn.jsdelivr.net url(...)อ้างอิงได้แค่ 4 host ข้างบน บวก Supabase Storage ของคุณเอง ห้าม hotlink รูปจากเว็บอื่น- inline data URI:
data:image/(png|jpe?g|webp|gif|svg+xml);…— ดูส่วน SVG ข้างล่าง
ใช้ไม่ได้
expression(...)(ของเก่ายุค IE)behavior:,-moz-binding:,binding:- URL ที่ขึ้นต้นด้วย
javascript:ทุกที่ @importหรือurl(...)ที่ชี้ไป host นอก allowlist
ฝัง SVG
ฝัง SVG ใน CSS ได้ตรงๆ ผ่าน data: URI — ไม่ต้องอัปไฟล์ ไม่ต้องมี asset host คลื่น รูปทรงอิสระ ลาย repeat ลายมือวาด particle เคลื่อนไหว ทำได้หมด นี่คือขั้นตอนเต็ม
ขั้น 1 — ออกแบบ SVG
เลือกเครื่องมือที่ถนัด:
- Figma — ออกแบบเสร็จแล้ว คลิกขวา → Copy as SVG หรือ File → Export → SVG รูปเดียวๆ ใช้ได้ดีสุด — ถ้าจัดเป็น group มันจะติด wrapper มาเกินจำเป็น
- Inkscape (ฟรี ใช้บน desktop) — vector editor เต็มรูปแบบFile → Save as → Optimised SVG ตัด metadata ให้เยอะแล้ว
- เครื่องมือสำเร็จรูป — ทำรูปทรงพื้นฐานเร็วๆ: haikei.app (คลื่น blob shape กระจาย), getwaves.io (คลื่นอย่างเดียว ง่ายมาก), svgbackgrounds.com (ลาย repeat)
- เขียนเอง — รูปทรงง่ายๆ แค่
<path d="M 0,40 Q 50,80 100,40" />ก็สั้นกว่าที่ export มา — อ้างอิง SVG path ดูจาก MDN ได้ครบ
ขั้น 2 — ลดขนาดให้เล็กลง
SVG ที่ export จาก designer tool ติด metadata เยอะ (จาก Figma / Illustrator + comment + group wrapper) ลอดผ่านตัวพวกนี้ก่อน:
- SVGOMG (เว็บ ฟรี) — แค่ลาก SVG ใส่ มัน minify ให้ + เปิดปิดแต่ละ transform ได้ ค่าเริ่มต้นใช้ได้เลย เปิด Prefer viewBox กับ Multipass ให้ลดอีก ~10%
- svgo (npm) — engine เดียวกัน เขียน script ได้
ขั้น 3 — URL-encode ทำเป็น data URI
CSS url(...) รับเป็น string — แต่ SVG มีตัวอักษรที่ต้อง escape ก่อนเอาไปใช้ใน CSS ได้ สามกฎ:
- ใช้ single quote ใน attribute ของ SVG (
fill='white') เพื่อให้ string CSS ข้างนอกใช้ double quote ครอบ URI ทั้งก้อนได้ - แทน
#ด้วย%23— สำคัญสำหรับ สี hex (#fff→%23fff) และทุกurl(#gradId)ที่ชี้ไป def ใน SVG เอง - แทน
%ด้วย%25— เฉพาะตอนมี CSS keyframe ใน SVG (0%→0%25)
เครื่องมือที่ทำให้:
- URL encoder for SVG ของ yoksel — วาง SVG ดิบลงไป ก๊อปเอา string ที่พร้อมใช้ใน CSS ออกมา มี data URI prefix ครอบให้แล้ว
- npm:
mini-svg-data-uri— ถ้า generate CSS ผ่าน script
ขั้น 4 — เอาไปใช้ใน CSS
วางผลลัพธ์ลงใน background-image หรือ mask-image ผสมกับ gradient, position, repeat ได้ตามใจ
/* คลื่นที่ก้น hero ของ linkfolio */
.fh-linkfolio {
background:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 80' preserveAspectRatio='none'><path d='M0,40 C300,80 600,0 1200,40 L1200,80 L0,80 Z' fill='%23ffffff' fill-opacity='0.4'/></svg>") bottom / 100% 80px no-repeat,
linear-gradient(180deg, #e0f2fe, #38bdf8);
}ตัวอย่าง
ก๊อปไปแก้ ผ่าน sanitizer หมด
ลายจุดซ้ำ
.fh-page {
background-image:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><circle cx='12' cy='12' r='1.2' fill='%23000000' fill-opacity='0.08'/></svg>");
background-size: 24px 24px;
}ฟองน้ำลอยขึ้น (CSS ใน SVG)
.fh-linkfolio::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 800' preserveAspectRatio='xMidYMid slice'><style>@keyframes r{0%25{transform:translateY(40px);opacity:0}10%25{opacity:.6}100%25{transform:translateY(-840px);opacity:0}}circle{animation:r linear infinite}</style><circle cx='80' cy='800' r='4' fill='white' style='animation-duration:18s'/><circle cx='220' cy='800' r='6' fill='white' style='animation-duration:22s;animation-delay:-7s'/></svg>");
background-size: 400px 800px;
background-repeat: repeat;
}สังเกต %25 ใน 0%25 และ 10%25 — นั่นคือ keyframe selector ของ CSS ภายใน SVG เลยต้อง encode %
ขีดเส้นใต้แบบลายมือใต้ชื่อ
.fh-display-name {
display: inline-block;
background:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 12' preserveAspectRatio='none'><path d='M0,6 Q40,2 100,6 T200,6' stroke='%23f59e0b' stroke-width='3' fill='none' stroke-linecap='round'/></svg>") bottom / 100% 8px no-repeat;
padding-bottom: 4px;
}รูปแบบ animation
สองรูปแบบนี้ครอบคลุมที่คุณต้องใช้เกือบหมด ทั้งคู่กินทรัพยากรน้อยกว่าที่ตาเห็น
CSS keyframes (พื้นฐาน แต่เร็ว)
ประกาศ @keyframes หนึ่งครั้ง แล้วเอาไปใส่ผ่าน animation ตรงไหนก็ได้ ใช้แต่ property ที่ GPU ชอบ: transform, opacity, filter อย่า animate width, height, top, left — พวกนี้บังคับให้ browser คำนวณ layout ทุกเฟรม
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.fh-avatar {
animation: float 4s ease-in-out infinite;
}SVG ที่มี animation ฝังเป็น background
SVG ที่ใช้เป็น background-image จะรัน CSS animation ภายในตัวมันเอง ใส่ <style> ไว้ใน SVG ชี้ไปที่ element ที่วาด แล้ว repeat ทั้งภาพผ่าน background-repeat ปรับ animation-delay + animation-duration ต่างกันในแต่ละ element เพื่อให้รอยต่อของ repeat กลืนไป ดูตัวอย่างในส่วน SVG ข้างบน
เรื่อง performance
backdrop-filter: blur(...)คือ property ที่กิน CPU หนักสุดในชีวิตประจำวัน — ทุกเฟรมที่ scroll ต้อง re-blur layer ด้านหลังใหม่หมด ใช้บนพื้นที่เล็กๆ เท่านั้น หรือไม่ใช้เลย ถ้าพื้นหลังเป็น gradient เรียบๆ ใส่rgba(255,255,255,.65)ทึบๆ ดูเหมือนแก้วฝ้านุ่มได้ฟรี- animation ของ SVG background รันบน compositor ก็จริง แต่ต้องทาสีใหม่ทุก tile ทุกรอบ tile ใหญ่ + element animation น้อย = เบากว่า tile เล็กที่มี element เยอะ
- animation ที่ไม่ใช่ของจำเป็น ให้คู่กับ
prefers-reduced-motionเสมอ คนที่ปิด motion จะเห็นเวอร์ชันนิ่งแทน:
@media (prefers-reduced-motion: reduce) {
.fh-avatar { animation: none; }
.fh-linkfolio::after { display: none; }
}คำถามที่เจอบ่อย
▸บันทึกไม่ผ่าน — “url(...) is blocked”
▸บันทึกไม่ผ่าน — “url('%23gradId') is blocked”
# เปลี่ยนเป็น %23 ใน data URI ปกติ encoder ทำให้อยู่แล้ว▸animation ใน SVG ที่ฝังไว้ไม่รัน
% ใน keyframe selector (0% → 0%25) SVG parser เลยอ่าน syntax พัง (2) เอา SVG ไปใช้เป็น <img> หรือ mask-image แทนที่จะเป็น background-image — browser ส่วนใหญ่รัน CSS animation ภายใน SVG เฉพาะตอนใช้เป็น background เท่านั้น▸จะเขียน CSS เจาะจงไปที่ลิงก์ที่สร้างเองตัวเดียวยังไง
shop) แล้วเขียนถึงผ่าน .fh-link-custom-shop▸เขียน JavaScript ได้ไหม
javascript: URL, expression, binding) animation กับ interaction ต้องเป็น CSS-only — ซึ่งในปี 2026 ทำได้เยอะมากแล้ว (:has(), :focus-within, scroll-driven animation ฯลฯ)พร้อมลงมือยัง
เปิด editor แล้วลองเลย ทุกครั้งที่บันทึก ระบบเก็บเวอร์ชันก่อนหน้าให้อัตโนมัติ — พังไปก็กดย้อนกลับได้