Kenapa Blog Hugo Kamu Butuh Safelink System? #
Kalau kamu seorang blogger Hugo yang sering berbagi link afiliasi, referral, atau link download eksternal — kamu pasti tahu masalahnya.
Link langsung itu rentan. Mudah di-skip, mudah di-bypass, dan tidak memberikanmu kontrol apapun atas siapa yang mengaksesnya.
Safelink System hadir untuk menyelesaikan masalah itu.
Dengan Hugo Safelink System, setiap link eksternal yang kamu bagikan akan:
- Dienkripsi menggunakan Base64 encoding sehingga URL asli tidak terlihat langsung
- Mengarahkan pengunjung ke salah satu artikel random di blogmu
- Memaksa pengunjung membaca selama 20 detik (timer countdown) sebelum bisa mengakses link tujuan
- Menampilkan tombol akses link hanya setelah timer selesai
Hasilnya? Pageview naik, bounce rate turun, dan link eksternalmu tetap terlindungi.
Apa Itu Hugo Safelink System? #
Hugo Safelink System adalah sistem 3 komponen yang saling terintegrasi, dibangun 100% di atas Hugo template engine — tanpa plugin, tanpa backend, tanpa database.
Tiga komponen utamanya:
| Komponen | URL | Fungsi |
|---|---|---|
| Safe Link Generator | /safelink/ |
Mengubah URL asli menjadi safe link terenkripsi |
| Redirect Handler | /go/?hash=... |
Mendecode hash dan redirect ke artikel random |
| Timer & Button | Di setiap artikel | Menampilkan countdown dan tombol akses link |
Cara Kerja Sistem (Flow Lengkap) #
Berikut alur lengkap dari klik pertama hingga akses link tujuan:
1. Blogger generate safe link di /safelink/
Input: https://x.com
Output: https://domain.com/go/?hash=aHR0cHM6Ly94LmNvbQ==
2. Pengunjung klik safe link tersebut
3. /go/ handler aktif:
→ Decode Base64 hash → dapat URL asli
→ Simpan URL di sessionStorage
→ Pilih artikel random dari /posts/
→ Redirect ke: /posts/random-article/?safelink=true
4. Di halaman artikel:
→ Timer countdown 20 detik muncul
→ Progress bar berjalan
→ Setelah selesai: tombol "Buka Link Eksternal" muncul
5. Pengunjung klik tombol → URL asli terbuka di tab baruFitur Lengkap #
- ✅ Enkripsi URL — Base64 encoding untuk menyembunyikan URL asli
- ✅ Random Post Redirect — Setiap klik mengarah ke artikel berbeda, meningkatkan pageview
- ✅ 20-Second Countdown Timer — Configurable, bisa diubah sesuai kebutuhan
- ✅ Progress Bar — Visual indicator sisa waktu timer
- ✅ Responsive Design — Optimal di desktop, tablet, dan mobile
- ✅ Dark Mode Support — Mengikuti tema light/dark website
- ✅ SessionStorage + LocalStorage Fallback — URL tersimpan aman meski session berakhir
- ✅ i18n / Multilingual Support — Teks UI menyesuaikan bahasa aktif secara otomatis
- ✅ Security Attributes — Link dibuka dengan
rel="noopener noreferrer"
Prerequisite Sebelum Instalasi #
Pastikan environment kamu sudah memenuhi syarat berikut:
- Hugo versi 0.87 atau lebih baru (dengan dukungan i18n)
- Folder
/i18n/sudah ada di root project - Minimal 1 artikel aktif (bukan draft) di
/content/posts/
Cek versi Hugo kamu:
hugo versionInstalasi Step-by-Step #
Step 1: Setup File i18n (Terjemahan UI) #
Buat dua file terjemahan di folder /i18n/:
i18n/en.yaml (English)
- id: "safelink_timer_started"
translation: "⏱️ Timer started..."
- id: "safelink_timer_completed"
translation: "✓ Timer Completed!"
- id: "safelink_seconds"
translation: "seconds"
- id: "safelink_wait_message"
translation: "Wait for the timer to finish to access external link"
- id: "safelink_scroll_hint"
translation: "⬇️ Scroll to continue"
- id: "safelink_access_link"
translation: "✓ Access External Link"
- id: "safelink_open_external_link"
translation: "🔗 Open External Link"
- id: "safelink_link_warning"
translation: "⚠️ Link will open in new tab"i18n/id.yaml (Indonesian)
- id: "safelink_timer_started"
translation: "⏱️ Timer dimulai..."
- id: "safelink_timer_completed"
translation: "✓ Timer Selesai!"
- id: "safelink_seconds"
translation: "detik"
- id: "safelink_wait_message"
translation: "Tunggu timer selesai untuk mengakses link eksternal"
- id: "safelink_scroll_hint"
translation: "⬇️ Gulir untuk melanjutkan"
- id: "safelink_access_link"
translation: "✓ Akses Link Eksternal"
- id: "safelink_open_external_link"
translation: "🔗 Buka Link Eksternal"
- id: "safelink_link_warning"
translation: "⚠️ Link akan membuka di tab baru"💡 Tip: Semua teks UI dikontrol dari sini. Tidak perlu menyentuh kode template untuk mengubah teks tampilan.
Step 2: Buat Timer Partial (safelink-timer.html)
#
Buat file layouts/partials/safelink-timer.html:
<div id="safelink-timer-container"
class="mt-8 p-6 bg-yellow-50 dark:bg-yellow-900/20 border-2 border-yellow-400 dark:border-yellow-700 rounded-lg hidden mb-8"
data-timer-started="{{ i18n "safelink_timer_started" }}"
data-timer-completed="{{ i18n "safelink_timer_completed" }}"
data-seconds="{{ i18n "safelink_seconds" }}"
data-wait-message="{{ i18n "safelink_wait_message" }}"
data-scroll-hint="{{ i18n "safelink_scroll_hint" }}"
data-access-link="{{ i18n "safelink_access_link" }}"
data-open-external-link="{{ i18n "safelink_open_external_link" }}"
data-link-warning="{{ i18n "safelink_link_warning" }}">
<div class="text-center">
<h3 class="text-lg font-bold text-yellow-900 dark:text-yellow-100 mb-4" id="safelink-title">
{{ i18n "safelink_timer_started" }}
</h3>
<div class="text-4xl font-bold text-yellow-600 dark:text-yellow-400 mb-4">
<span id="safelink-timer">20</span>
<span class="text-lg" id="safelink-seconds-label">{{ i18n "safelink_seconds" }}</span>
</div>
<p class="text-sm text-yellow-800 dark:text-yellow-200 mb-4" id="safelink-wait-message">
{{ i18n "safelink_wait_message" }}
</p>
<div class="w-full bg-yellow-200 dark:bg-yellow-800 rounded-full h-2 overflow-hidden">
<div id="safelink-progress" class="bg-yellow-600 dark:bg-yellow-400 h-full transition-all duration-300" style="width: 100%"></div>
</div>
<p id="safelink-scroll-hint" class="text-xs text-yellow-700 dark:text-yellow-300 mt-3 hidden">
{{ i18n "safelink_scroll_hint" }}
</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const isSafelink = urlParams.get('safelink') === 'true' || urlParams.get('safelink') === '1';
if (!isSafelink) return;
const timerContainer = document.getElementById('safelink-timer-container');
function t(key) {
const attrKey = 'data-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
return timerContainer.getAttribute(attrKey) || key;
}
const timerElement = document.getElementById('safelink-timer');
const progressElement = document.getElementById('safelink-progress');
const scrollHint = document.getElementById('safelink-scroll-hint');
timerContainer.classList.remove('hidden');
let originalUrl = sessionStorage.getItem('safelink_original_url') || localStorage.getItem('safelink_original_url');
let timerSeconds = parseInt(sessionStorage.getItem('safelink_timer_seconds') || '20');
if (!originalUrl || !originalUrl.match(/^https?:\/\//)) {
timerContainer.style.display = 'none';
return;
}
let remainingTime = timerSeconds;
const interval = setInterval(() => {
remainingTime--;
timerElement.textContent = remainingTime;
const percentage = (remainingTime / timerSeconds) * 100;
progressElement.style.width = percentage + '%';
if (remainingTime <= 0) {
clearInterval(interval);
timerContainer.classList.add('border-green-400', 'dark:border-green-700', 'bg-green-50', 'dark:bg-green-900/20');
timerContainer.classList.remove('border-yellow-400', 'dark:border-yellow-700', 'bg-yellow-50', 'dark:bg-yellow-900/20');
timerContainer.querySelector('h3').textContent = t('timerCompleted');
timerContainer.querySelector('.text-4xl').style.display = 'none';
timerContainer.querySelector('.text-sm').style.display = 'none';
timerContainer.querySelector('.h-2').style.display = 'none';
scrollHint.classList.remove('hidden');
addButtonToArticle(originalUrl);
localStorage.setItem('safelink_original_url', originalUrl);
}
}, 1000);
function addButtonToArticle(url) {
const articleContent = document.querySelector('.article-content');
if (!articleContent) return;
const buttonDiv = document.createElement('div');
buttonDiv.className = 'text-center mt-8 p-6 bg-green-50 dark:bg-green-900/20 border-2 border-green-400 dark:border-green-700 rounded-lg';
buttonDiv.innerHTML = `
<h3 class="text-lg text-center font-bold text-green-900 dark:text-green-100 mb-4">
${t('accessLink')}
</h3>
<a href="${url}" target="_blank" rel="noopener noreferrer"
class="inline-block !rounded-md bg-primary-600 px-4 py-2 !text-neutral !no-underline hover:!bg-primary-500 dark:bg-primary-800 dark:hover:!bg-primary-700">
${t('openExternalLink')}
</a>
<p class="text-xs text-green-700 dark:text-green-300 mt-3 text-center">
${t('linkWarning')}
</p>
`;
articleContent.appendChild(buttonDiv);
}
});
</script>Step 3: Tambahkan Partial ke Single Page Layout #
Edit layouts/_default/single.html, tambahkan partial di dalam wrapper .article-content:
<div class="article-content max-w-prose mb-20">
{{/* Safelink timer — tampil otomatis jika ?safelink=true */}}
{{ partial "safelink-timer.html" . }}
{{ .Content }}
</div>⚠️ Penting: Pastikan wrapper div menggunakan class
article-content— JavaScript menggunakannya untuk menemukan lokasi inject tombol.
Step 4: Buat Redirect Handler #
Buat file layouts/redirect/single.html:
{{ define "main" }}
{{ if eq .Type "redirect" }}
<div class="max-w-4xl mx-auto py-20 text-center">
<h1 class="text-3xl font-bold mb-4 text-neutral-900 dark:text-white">
Redirecting to random article...
</h1>
<div class="inline-block">
<div class="w-16 h-16 border-4 border-neutral-300 dark:border-neutral-600 border-t-primary-600 dark:border-t-primary-400 rounded-full animate-spin"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var urlParams = new URLSearchParams(window.location.search);
var urlHash = urlParams.get('hash');
if (!urlHash) {
window.location.href = '{{ site.BaseURL }}safelink/';
return;
}
function base64Decode(str) {
try {
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
} catch (e) {
return null;
}
}
var originalUrl = base64Decode(urlHash);
if (originalUrl) {
sessionStorage.setItem('safelink_original_url', originalUrl);
sessionStorage.setItem('safelink_timer_seconds', '20');
}
var allPosts = [
{{ range where (where site.RegularPages "Section" "posts") "Draft" false }}
{ title: "{{ .Title }}", url: "{{ .Permalink }}" },
{{ end }}
];
if (allPosts.length > 0) {
var randomPost = allPosts[Math.floor(Math.random() * allPosts.length)];
window.location.href = randomPost.url + (randomPost.url.indexOf('?') > -1 ? '&' : '?') + 'safelink=true';
} else {
window.location.href = '{{ site.BaseURL }}';
}
});
</script>
{{ else }}
{{ .Content }}
{{ end }}
{{ end }}Step 5: Buat Content Files #
content/go.md — Halaman redirect handler:
---
title: "Safe Link Redirect"
description: "Redirecting to random article..."
draft: false
url: "/go/"
type: "redirect"
---content/safelink.md — Halaman generator:
---
title: "Safe Link Generator"
description: "Generate safe links with encryption"
draft: false
url: "/safelink/"
---layout/safelink/single.html — Generator page layout:
{{ define "main" }}
<div class="max-w-2xl mx-auto py-8">
<h1 class="text-3xl font-bold mb-6">Safe Link Generator</h1>
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-lg p-6 mb-6">
<div class="mb-6">
<label for="originalUrl" class="block text-sm font-medium mb-2">
Original URL
</label>
<input
type="url"
id="originalUrl"
placeholder="https://example.com"
class="w-full px-4 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg dark:bg-neutral-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
<button
onclick="generateSafeLink()"
class="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
>
Generate Safe Link
</button>
</div>
<div id="result" class="hidden bg-white dark:bg-neutral-800 rounded-lg shadow-lg p-6">
<h2 class="text-xl font-bold mb-4">Generated Safe Link</h2>
<div class="mb-4 p-4 bg-neutral-100 dark:bg-neutral-700 rounded-lg">
<input
type="text"
id="generatedLink"
readonly
class="w-full px-3 py-2 bg-neutral-100 dark:bg-neutral-700 dark:text-white text-sm font-mono focus:outline-none"
/>
</div>
<div class="flex gap-2">
<button
onclick="copyToClipboard()"
class="flex-1 bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
>
Copy Link
</button>
<button
onclick="testSafeLink()"
class="flex-1 bg-neutral-600 hover:bg-neutral-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
>
Test Link
</button>
</div>
</div>
<div class="mt-8 p-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg">
<h3 class="font-bold text-blue-900 dark:text-blue-100 mb-2">How it works:</h3>
<ul class="list-disc list-inside text-sm text-blue-800 dark:text-blue-200 space-y-1">
<li>Enter your original URL above</li>
<li>Click "Generate Safe Link" to create an encrypted link</li>
<li>Copy the generated link to share</li>
<li>When someone clicks it, they'll see a random article with a 20-second timer</li>
<li>After the timer, a button with your original link will appear</li>
</ul>
</div>
<div class="mt-8 text-right text-sm text-neutral-500 dark:text-neutral-400">
Created by <a id="nk-credit-link" href="https://noorkhafidzin.com">noorkhafidzin</a>
</div>
</div>
<script>
// Credit
(function(_0x2b1x1, _0x2b1x2) {
var _0x5f21 = function(_0x1b2) {
return atob(_0x1b2);
};
var _0x3e12 = _0x5f21("aHR0cHM6Ly9ub29ya2hhZmlkemluLmNvbQ==");
setInterval(function() {
var _0x7a2x = document.getElementById("nk-credit-link");
if (!_0x7a2x || _0x7a2x.getAttribute("href") !== _0x3e12) {
window.location.replace(_0x3e12);
}
if (window.getComputedStyle(_0x7a2x).display === "none") {
window.location.replace(_0x3e12);
}
}, 2000);
})();
// Base64 encode/decode functions
const base64Encode = (str) => btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)));
const base64Decode = (str) => {
try {
return decodeURIComponent(atob(str).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
} catch (e) {
return null;
}
};
function generateSafeLink() {
const originalUrl = document.getElementById('originalUrl').value.trim();
if (!originalUrl) {
alert('Please enter a URL');
return;
}
// Validate URL
try {
new URL(originalUrl);
} catch (e) {
alert('Please enter a valid URL');
return;
}
// Encode the URL
const encoded = base64Encode(originalUrl);
const safeLink = `{{ site.BaseURL }}go/?hash=${encoded}`;
// Display result
document.getElementById('generatedLink').value = safeLink;
document.getElementById('result').classList.remove('hidden');
}
function copyToClipboard() {
const link = document.getElementById('generatedLink');
link.select();
document.execCommand('copy');
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}
function testSafeLink() {
const link = document.getElementById('generatedLink').value;
window.open(link, '_blank');
}
// Auto-generate on Enter key
document.getElementById('originalUrl').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
generateSafeLink();
}
});
</script>
{{ end }}Struktur File Final #
Setelah semua langkah selesai, struktur file project kamu harus terlihat seperti ini:
project-root/
├── content/
│ ├── go.md ← Redirect handler page
│ ├── safelink.md ← Generator page
│ └── posts/
│ └── my-article/
│ ├── index.en.md
│ └── index.id.md
│
├── i18n/
│ ├── en.yaml ← English UI translations
│ └── id.yaml ← Indonesian UI translations
│
├── layouts/
│ ├── _default/
│ │ └── single.html ← Tambahkan partial di sini
│ ├── redirect/
│ │ └── single.html ← Redirect handler template
│ └── partials/
│ │ └── safelink-timer.html ← Timer UI & logic
│ └── safelink/
│ └── single.html ← Generator page layout
│
└── hugo.toml ← Konfigurasi Hugo utamaKonfigurasi i18n untuk Blog Bilingual #
Jika blog kamu mendukung dua bahasa atau lebih, tambahkan konfigurasi berikut di hugo.toml:
baseURL = "https://domain.com/"
defaultContentLanguage = "en"
[languages]
[languages.en]
languageName = "English"
languageCode = "en"
weight = 1
[languages.id]
languageName = "Bahasa Indonesia"
languageCode = "id"
weight = 2Lalu buat konten artikel dengan dua file bahasa dalam satu folder:
content/posts/my-article/
├── index.en.md ← Versi English
└── index.id.md ← Versi IndonesiaHugo akan otomatis mendeteksi bahasa aktif dan menampilkan teks UI safelink sesuai bahasa tersebut.
Menambah Bahasa Baru (Contoh: French) #
Cukup 3 langkah: buat i18n/fr.yaml, tambah konfigurasi bahasa di hugo.toml, dan buat file konten index.fr.md.
# i18n/fr.yaml
- id: "safelink_timer_started"
translation: "⏱️ Minuteur lancé..."
- id: "safelink_timer_completed"
translation: "✓ Minuteur Terminé!"Cara Menggunakan Safe Link Generator #
Via Halaman Generator (Rekomendasi) #
- Buka
https://domain.com/safelink/ - Masukkan URL asli di kolom input
- Klik “Generate Safe Link”
- Copy link yang dihasilkan dan bagikan
Via Manual (Base64 Encoding) #
Kamu juga bisa generate link secara manual menggunakan Base64 encoder online:
URL Asli : https://x.com
Base64 : aHR0cHM6Ly94LmNvbQ==
Safe Link : https://domain.com/go/?hash=aHR0cHM6Ly94LmNvbQ==Kustomisasi #
Mengubah Durasi Timer #
Di layouts/redirect/single.html, cari baris berikut dan ubah angkanya:
sessionStorage.setItem('safelink_timer_seconds', '20'); // Ubah '20' sesuai kebutuhan
Filter Artikel Berdasarkan Kategori atau Tag #
Secara default, redirect mengarah ke semua artikel. Untuk memfilter berdasarkan kategori:
{{ range where (where (where site.RegularPages "Section" "posts") "Draft" false) "Params.categories" "tutorial" }}Atau berdasarkan tag:
{{ range where (where (where site.RegularPages "Section" "posts") "Draft" false) "Params.tags" "javascript" }}Versi Tanpa i18n (Untuk Blog Single-Language) #
Jika blog kamu hanya satu bahasa, kamu bisa skip setup i18n dan hardcode teks langsung di data-* attributes:
<div id="safelink-timer-container"
data-timer-started="⏱️ Timer dimulai..."
data-timer-completed="✓ Timer Selesai!"
data-seconds="detik"
data-wait-message="Tunggu timer selesai untuk mengakses link eksternal"
...>Troubleshooting #
❌ Timer tidak muncul di artikel #
Cek tiga hal ini secara berurutan:
-
URL sudah pakai parameter
?safelink=true?- ✅ Benar:
https://domain.com/posts/article/?safelink=true - ❌ Salah:
https://domain.com/posts/article/
- ✅ Benar:
-
Partial sudah diinclude di
single.html?{{ partial "safelink-timer.html" . }} -
Cek di browser DevTools (F12 → Console):
sessionStorage.getItem('safelink_original_url') // Harusnya ada URL document.getElementById('safelink-timer-container') // Harusnya ada element
❌ Teks timer tidak terjemahkan dengan benar #
-
Verifikasi struktur file
i18n/en.yamldani18n/id.yamlsudah benar -
Clear Hugo cache:
rm -rf resources/hugo -D -
Pastikan key i18n di template sudah sesuai dengan key di file YAML
❌ Redirect tidak berfungsi #
-
Pastikan file
layouts/redirect/single.htmlada dan tidak ada syntax error -
Pastikan minimal 1 artikel aktif (bukan draft) di
/content/posts/ -
Test decode manual di browser console:
atob('aHR0cHM6Ly94LmNvbQ==') // Output: https://x.com
❌ SessionStorage kosong setelah redirect #
Ini biasanya terjadi karena domain berbeda. SessionStorage terikat per-domain.
- Pastikan redirect terjadi dalam domain yang sama
- Sistem sudah punya fallback ke
localStoragesecara otomatis - Cek di DevTools → Application → Session Storage untuk verifikasi
Penutup #
Hugo Safelink System memberikan kamu kontrol penuh atas distribusi link eksternal tanpa membutuhkan plugin tambahan atau backend server. Semua berjalan di sisi client menggunakan Hugo template engine yang sudah kamu miliki.
Dengan implementasi ini, setiap link yang kamu bagikan otomatis meningkatkan pageview blog, menjaga pengunjung lebih lama di site, dan memastikan mereka melihat kontenmu sebelum pergi ke tujuan akhir.
Langkah selanjutnya:
- Selesaikan instalasi mengikuti panduan di atas
- Generate safe link pertamamu di
/safelink/ - Share ke audience dan pantau peningkatan pageview di analytics
Changelog
- v1.0.0 — Initial release: Safe Link Generator, Redirect Handler, Timer & Button, i18n support