Jekyll Secret Posts

Jekyll URL Share-only 비밀 문서 생성 플러그인

Feb 2026

Jekyll Secret Posts

TL;DR

  • Share-Only URL 방식으로 비공개 포스트를 제공하는 Jekyll 플러그인입니다.
  • 민감한 내용을 URL을 아는 사람만 접근 가능하게 블로그에 올리기 위해 개발했습니다.
  • 해시 기반 permalink, sitemap·검색엔진 제외, 간단한 연동을 지원합니다.


기획

배경

Resume Image

저는 요즘 취업을 위해 이력서를 쓰고 있습니다. 제 경력, 진행한 프로젝트들을 비롯한 지금까지의 경험을 담아내고 있는데, 어떻게 모든 내용을 이력서 안에 담아낼 수 있을지 고민하게 되었습니다.

이력서 안에 프로젝트에 대한 모든 설명을 넣을 수는 없습니다. 그랬다가는 이력서가 10페이지도 넘어버릴 테니까요. 그렇다고 설명을 뺀다면, 문제 해결 과정을 제대로 담아낼 수 없습니다.

그래서 저는 상세한 내용을 담은 별도의 외부 문서를 만들어, 이력서에서는 간단한 설명만 제공하고 디테일한 경험은 외부 문서에서 풀어내는 방법을 선택했습니다.

그렇다면 외부 문서는 어떻게 만들어야 할까요? 파일을 또 만들자니 이력서에 담는 것과 다를 바 없었고, Notion이나 Google Docs를 쓰자니 디자인이 마음에 들지 않았습니다.

Blog Projects Page

그때, 제 블로그가 눈에 들어왔습니다. 디자인도 괜찮고, UI를 마음대로 커스텀할 수 있으며, URL만 공유하면 바로 접속할 수 있기에 최고의 대안이었습니다.

그러나 회사의 소프트웨어 아키텍처와 도메인적 맥락이 담긴 경력에서의 경험들을 블로그에 공개적으로 쓰는 것은 문제의 소지가 있었기에, 블로그를 그대로 사용할 수는 없었습니다.

단순히 특정 글을 목록에 나타나지 않게 처리할 수도 있었으나, 민감한 주제인 만큼 더 확실하고 확장 가능한 방법을 사용하고 싶었습니다.

Google Drive Link Popup

drive.google.com/drive/folders/1FsvTO123Bb3mSQu456HwSAdpD00mo6tM?usp=sharing

방법을 고민하던 중, Google Drive의 공유 URL 기능이 눈에 들어왔습니다. URL을 아는 사람은 누구나 접근할 수 있지만 모르는 사람이 정확히 찾는 것은 수학적으로 불가능에 가까운 Share-Only URL.

이는 제 요구 사항에 정확히 들어맞았고, Share-Only URL 생성을 자동화하는 Jekyll 플러그인을 만들기로 결정했습니다.


목표

1. Share-Only URL

숨겨진 아티클은 반드시 URL을 아는 사람만이 접근할 수 있어야 합니다.

그러므로 숨겨진 아티클은 아티클 목록에 노출하지 않는 것뿐만 아니라 Sitemap에도 나타나지 않아야 하고, 검색 엔진의 인덱싱도 피할 수 있어야 합니다.

또한, 아무리 잘 숨기더라도 유추나 무작위 대입으로 URL을 알아낼 수 있다면 무용지물이기에 Google Drive의 예처럼 유추가 불가능한 난수 문자열을 사용해야 합니다.


2. 매끄러운 연동

다른 Jekyll 플러그인들의 환경 설정에 고생한 경험이 있기에, 직접 개발하는 플러그인에서는 매끄러운 개발 경험을 제공하고 싶었습니다.

그러므로 README만 읽으면 1분만에 연동이 끝나고, 커스텀 설정은 최소한의 필요 기능들만을 최소한의 인터페이스로 제공하도록 구성해야 했습니다.

또한, 제 블로그를 비롯한 많은 Jekyll 웹사이트들은 대부분 여러 플러그인들을 사용하여 구축되기에, 복잡한 설정 없는 매끄러운 연동을 구현하기 위해 다른 플러그인과의 호환성을 확보해야 했습니다.


개발

기술 스택

Ruby 2.7 이상, Jekyll 4.x 환경을 기반으로 개발하였습니다.

Jekyll 웹사이트와 통합해야 하는 플러그인인 점을 고려하여 Jekyll 외의 추가 의존성을 배제하고 Ruby 표준 라이브러리만을 사용하여 구현하였습니다. 테스트 프레임워크와 린터는 Ruby 생태계에서 많이 사용되는 RSpec과 RuboCop을 선택했습니다.


아키텍처

flowchart LR subgraph INPUT["Input"] config["_config.yml"] env["ENV"] md["_secret/"] end Config["Config"] UrlTokenizer["UrlTokenizer"] Hooks["Hooks"] Generator["Generator"] subgraph OUTPUT["Output"] secret["Secret posts"] redirect["Redirect"] end config --> Config env --> Config md -->|"documents"| Hooks Config -->|"settings (salt, token_length)"| UrlTokenizer Config -->|"settings"| Hooks Config -->|"settings"| Generator UrlTokenizer -->|"token"| Hooks UrlTokenizer -->|"token"| Generator Hooks -->|"permalink, noindex"| secret Generator -->|"redirect page"| redirect

본 플러그인은 크게 Config, UrlTokenizer, Hooks, Generator 네 모듈로 구성됩니다.


Config

flowchart LR subgraph INPUT["Input"] yaml["_config.yml\nsecret_posts"] baseurl["baseurl"] env["JEKYLL_SECRET_SALT"] end subgraph CONFIG["Config"] read["Read & validate"] fallback["Default fallback"] normalize["Normalize"] end subgraph OUTPUT["Output"] s_dir["source_dir"] coll["collection_name"] prefix["url_prefix"] salt["salt"] layout["secret_index_layout"] redirect["redirect_url"] len["token_length"] list["list_urls"] end yaml --> read baseurl --> read env --> read read --> fallback fallback --> normalize normalize --> s_dir normalize --> coll normalize --> prefix read --> salt normalize --> layout normalize --> redirect normalize --> list

Config는 Jekyll site 설정과 _config.yml의 커스텀 설정, Salt 환경 변수(JEKYLL_SECRET_SALT)를 읽어 빌드 Lifecycle 전체에서 참조될 전역 설정값을 제공합니다.

커스텀 설정은 전부 Optional로, 비밀 Collection의 경로와 식별자, 비밀 문서가 위치할 URL prefix 등 Jekyll의 기본 설정과 호환되는 최소한의 설정만을 제공합니다. 자세한 설명은 README.md를 참조해주세요.


UrlTokenizer

flowchart LR subgraph IN["Input"] c["Config"] lbl["collection_label"] pth["relative_path"] end subgraph TOK["UrlTokenizer"] t["token_for"] end OUT["token"] c --> t lbl --> t pth --> t t -->|"salt + label + path → SHA256 → trim"| OUT

UrlTokenizer는 비밀 컬렉션 식별자(collection_name)와 상대 경로(source_dir)를 해시하여 URL로 사용될 고정 길이의 hex 토큰을 생성합니다.

Salt 없이 해시를 계산할 경우 추측을 통한 대입 공격에 노출되기에 실제 배포 시 보안을 위해 사용을 권장하고 있습니다.

Salt로 SHA-256 해시를 계산하여 일부분을 토큰으로 사용합니다. 같은 경로에는 항상 같은 토큰이 나오기에 Salt만 같다면 어떤 환경에서도 동일한 permalink를 보장할 수 있습니다.


Hooks

flowchart TB subgraph HOOKS["Hooks"] direction TB hook1["site:after_init"] hook2["documents:post_init"] hook3["documents:post_render"] end subgraph ACTION1["register_secret_collection"] a1_in["site"] a1_proc["Add secret collection\nRemove from exclude"] a1_out["site.config"] end subgraph ACTION2["apply_secret_permalink"] a2_in["doc"] a2_proc["Check secret doc\nGet token from UrlTokenizer\nSet permalink, sitemap"] a2_out["doc.data"] end subgraph ACTION3["inject_noindex"] a3_in["doc.output"] a3_proc["Check secret doc\nInsert noindex meta"] a3_out["doc.output"] end hook1 --> ACTION1 hook2 --> ACTION2 hook3 --> ACTION3 a1_in --> a1_proc --> a1_out a2_in --> a2_proc --> a2_out a3_in --> a3_proc --> a3_out

Hooks는 비밀 컬렉션을 생성하고 검색 제외를 처리하여 플러그인의 핵심 기능을 구현하며, Jekyll 빌드의 Lifecycle을 따라 세 번 발동됩니다.

site 초기화 이후 - 비밀 컬렉션을 Jekyll collections에 등록

document 초기화 이후 - UrlTokenizer 호출하여 비밀 컬렉션에 속한 문서의 URL permalink 생성, sitemap 제외 설정

렌더링 이후 - 비밀 컬렉션의 렌더링된 HTML에 robots 메타 태그 삽입하여 검색 엔진 인덱싱 방지


Generator

flowchart LR subgraph INPUT["Input"] site["site"] end subgraph GEN["Generator"] a["add_secret_index_page"] l["log_secret_urls"] end subgraph OUT["Output"] page["Redirect page - index.html"] urls["URL list"] end site --> a site --> l a --> page l -->|"if list_urls"| urls

Generator는 비밀 문서 URL의 접근 방식을 관리하며, Jekyll 빌드 Lifecycle 중 렌더링 이전에 실행됩니다.

Jekyll은 빌드 시 비밀 문서 경로(_site/s/) 바로 아래에 index.html을 자동 생성하지 않고, _site/s/<token>/index.html과 같은 개별 문서만 생성합니다.

만약 웹 서버가 디렉터리 리스팅(Apache - mod_autoindex, Nginx - autoindex)을 지원할 경우, 비밀 경로 /s/에 접근 시 가려져야 할 하위 문서들의 URL들을 볼 수 있게 됩니다.

이를 방지하기 위해 Generator는 리다이렉트용 index.html 페이지를 생성하여 비밀 경로에 추가합니다. 이를 통해 비밀 경로에 접근할 시 설정에서 지정한 경로(redirect_url)로 자동 리다이렉트되도록 해 디렉터리 리스팅으로 인한 URL 유출을 차단할 수 있습니다.

더불어, Generator는 list_urls이 켜져 있을 경우 비밀 컬렉션의 URL을 빌드 로그에 출력하는 역할을 맡습니다. 이 기능의 설계에 대한 고민은 트러블슈팅 문단에서 후술하겠습니다.


Jekyll Lifecycle

flowchart LR subgraph LIFECYCLE["Jekyll Lifecycle"] direction LR s1["Init"] s2["Read"] s3["Generate"] s4["Render"] s5["Write"] end s1 --> s2 --> s3 --> s4 --> s5 h1["after_init\nregister_secret"] h2["post_init\napply_permalink"] g["Generator"] h3["post_render\ninject_noindex"] s1 -.-> h1 s2 -.-> h2 s3 -.-> g s4 -.-> h3

플러그인은 Jekyll 빌드 Lifecycle의 Init, Read, Generate, Render 네 시점을 따라 순차적으로 실행됩니다.

Init

Init 단계 직후 site:after_init 훅이 실행됩니다. 컬렉션 설정 확정 이전 시점에 비밀 포스트 컬렉션을 collections에 등록하고, 소스 디렉터리가 exclude에 포함돼 있으면 제거하여 Jekyll이 _secret/ 디렉터리를 읽도록 보장합니다.

Read

빌드 과정에서 문서 객체가 만들어질 때마다 documents:post_init 훅이 호출됩니다. 비밀 컬렉션에 속한 문서의 URL을 해시된 permalink로 지정합니다.

Generate

Generator 단계에서 플러그인 내부의 Generator가 실행됩니다. URL prefix 경로에 리다이렉트용 index.html을 추가하고, 설정에 따라 비밀 포스트 URL 목록을 빌드 로그에 출력합니다.

Render

문서 렌더링 이후, 각 문서의 HTML이 생성된 상태에서 documents:post_render 훅이 실행됩니다. 비밀 컬렉션 문서의 HTML 상단에 noindex 메타 태그를 삽입해 검색 엔진 인덱싱을 방지합니다.


트러블 슈팅

1. 비밀 포스트 URL 확인 방법

비밀 URL은 해시로 계산되기에 값을 에측할 수 없으므로, 사용자는 빌드된 _site 파일을 직접 까보지 않는 이상 비밀 URL을 알 수 없습니다.

이를 해결하기 위해, 빌드 시 출력되는 로그에서 URL을 확인할 수 있는 기능을 추가하였습니다.

하지만 만약 CI/CD나 외부 서버 등 안전하지 않은 환경에서 동일한 로그가 출력될 경우 비밀 URL이 유출될 수 있기에, 사용자가 개발 환경에서 수동으로 활성화한 경우에만 출력하도록 제한을 두어 설계하였습니다.


2. Jekyll 플러그인 충돌

Jekyll 플러그인들은 빌드 과정에서 컬렉션, 문서, permalink, 출력 HTML 같은 공통 객체들을 동시에 참조합니다. 따라서, 플러그인을 개발할 때는 다른 플러그인과 충돌하지 않도록 동시성을 고려한 설계가 필요합니다.

이 플러그인 또한 개발 과정에서 충돌 방지를 고민했습니다. 해결한 충돌 케이스들을 소개하겠습니다.

충돌 유형 원인 해결 방식
경로 충돌 기존 컬렉션과 이름/경로 중복 덮어쓰지 않도록 조치, 비밀 컬렉션 이름/경로 설정 지원
permalink 중복 수정 여러 플러그인이 같은 permalink를 수정 비밀 컬렉션으로 동작 범위 한정
sitemap 생성 jekyll-sitemap 플러그인이 Generator 단계에서 sitemap 수집 Generator 이전 documents:post_init 훅에서 비밀 문서에 doc.data["sitemap"] = false 설정


결과

GitHub Repository Preview Image Gem Version

개발한 플러그인을 GitHub와 RubyGems에 공개 배포했습니다. 프로젝트 GitHub Repository에서 사용해 보실 수 있습니다.

여러분이 읽고 계신 이 블로그에도 플러그인이 적용되어 블로그 어딘가, 저만 아는 URL에 경력 경험을 담은 글이 업로드되어 제 이력서를 채워주고 있습니다.

향후에도 직접 사용하며 필요성을 느낀 기능들을 추가해 나갈 예정입니다. 기여는 언제나 환영입니다!