TOC테스트중

TOC테스트중

Sur3원, h3r3하 y0u는 ar3대 g00d로 s3r해.

Casper Post 페이지, “인젝션”으로 바로 체감나는 4가지 핵심 패치

Casper의 포스트 본문 컨테이너는 .gh-content를 쓰고(테마의 post.hbs 기반), Code Injection으로 덮어씌우는 건 표준 전술이다. 아래 4가지는 UI/UX 효용 대비 리스크가 낮고, 최신 Casper 흐름과도 호환된다. (유령, GitHub)

스텝-바이-스텝(요약)

  1. Ghost Admin → Settings → Code injection → Site header에 아래 “인젝션 번들” 통째로 붙여넣기.
  2. 새로고침 후 포스트 하나 열고 확인.
  3. 필요 시 변수/옵션만 미세 조정.

4가지 코드, 왜 쓰는가

  1. 가독 리듬 & 열 폭 정렬(typography grid)
    • 목표: 줄길이 66~75자, 행간 1.7대, 문단·리스트 간격을 정규화해 읽기 피로 감소.
    • 포인트: .post-template .gh-content 범위만 타깃팅해 홈/태그 레이아웃과 충돌 없음.
  2. 자동 목차(TOC) + 앵커링
    • 목표: H2/H3에서 자동으로 목차 생성, 데스크톱에선 우측 스티키, 모바일에선 상단 블록.
    • 포인트: Casper가 쓰는 .gh-content 기준으로 헤딩을 스캔해 목차를 비침투식으로 삽입. (유령)
  3. 스크롤 리딩 프로그레스 바
    • 목표: 긴 글에서 현재 진행률을 1픽셀도 안 되는 얇은 바에 시각화 → 몰입감 증가.
    • 포인트: 본문 시작~끝 기준으로 계산, 헤더 겹침 방지(z-index) 처리.
  4. 미디어(이미지/갤러리) & 코드블록 가독 보강
    • 목표: 이미지 라운딩/캡션 정리, 갤러리 거터 균일화, 코드블록 복사 버튼으로 실용성 업.
    • 포인트: .kg-image-card / .kg-gallery-card / pre > code 등 Ghost 기본 클래스를 보수적 선택자로 스타일링.
참고: Casper는 Code Injection으로 레이아웃·스타일을 보강하는 사례가 널려 있고(포스트 헤더 높이/피처드 이미지 등), 굳이 테마 파일을 뜯지 않아도 충분히 간다. (Ghost Forum)

인젝션 번들(그냥 복붙)

위치: Settings → Code injection → Site header
역할: ①가독/열폭 ②자동 목차 ③프로그레스 바 ④미디어·코드 보강
<style>
/* ===== 0) 공통: 포스트 페이지 한정 스코프 ===== */
.post-template :root { --accent: var(--ghost-accent-color, #5e81ac); }

/* ===== 1) 가독 리듬 & 열폭 ===== */
.post-template .gh-content{
  max-width: 72ch;        /* 열 폭 */
  line-height: 1.75;      /* 행간 */
  font-size: clamp(1rem, 0.96rem + 0.3vw, 1.08rem);
  word-break: keep-all;
}
.post-template .gh-content p { margin: 0 0 1.05em; }
.post-template .gh-content h2,
.post-template .gh-content h3,
.post-template .gh-content h4{
  line-height: 1.25;
  margin: 2.1em 0 .65em;
  scroll-margin-top: 96px; /* 앵커 점프시 헤더 가림 방지 */
}
.post-template .gh-content li{ margin: .35em 0; }
.post-template .gh-content blockquote{
  border-left: 3px solid var(--accent);
  padding-left: 1rem; opacity:.95;
}

/* ===== 2) 자동 TOC 스타일 ===== */
.post-template .gh-toc{
  border: 1px solid rgba(0,0,0,.08);
  border-radius: 10px; padding: .9rem 1rem;
  background: rgba(127,127,127,.06);
  font-size: .95rem;
  margin: 1rem 0 1.5rem;
}
.post-template .gh-toc .gh-toc-title{ font-weight: 700; margin-bottom: .4rem; }
.post-template .gh-toc ol{ margin: 0; padding-left: 1.1rem; }
.post-template .gh-toc li{ margin: .25rem 0; }
@media (min-width: 1100px){
  .post-template .gh-toc{
    float: right; max-width: 260px;
    margin: 0 0 1rem 2rem; position: sticky; top: 96px;
  }
}

/* ===== 3) 리딩 프로그레스 바 ===== */
#reading-progress{
  position: fixed; top: 0; left: 0; height: 3px; width: 0%;
  background: var(--accent); z-index: 9999; transition: width .08s linear;
}

/* ===== 4) 미디어 & 코드 보강 ===== */
.post-template .gh-content figure.kg-image-card img{ border-radius: 10px; }
.post-template .gh-content figure.kg-gallery-card{ --gap: 8px; }
.post-template .gh-content figure.kg-gallery-card .kg-gallery-image img{ border-radius: 8px; }
.post-template .gh-content figure figcaption{
  color: rgba(0,0,0,.6); font-size: .9em; margin-top: .4rem; text-align: center;
}
.post-template pre{
  position: relative; overflow: auto; border-radius: 10px;
  padding-top: 2.4rem; /* 복사 버튼 영역 확보 */
}
.post-template .gh-copy{
  position: absolute; top: .5rem; right: .5rem;
  border: 1px solid rgba(0,0,0,.12); background: rgba(255,255,255,.85);
  padding: .35rem .6rem; border-radius: 8px; font-size: .9rem; cursor: pointer;
}
@media (prefers-color-scheme: dark){
  .post-template .gh-toc{ background: rgba(255,255,255,.04); border-color: rgba(255,255,255,.12); }
  .post-template .gh-copy{ background: rgba(0,0,0,.35); color: #fff; border-color: rgba(255,255,255,.22); }
}
</style>

<script>
/* ===== 안전 가드 ===== */
(function(){
  if(!document.body.classList.contains('post-template')) return;

  /* === 2) 자동 TOC 생성 === */
  const content = document.querySelector('.gh-content');
  if(content){
    const headings = content.querySelectorAll('h2, h3');
    if(headings.length){
      // id 없으면 만들어주기
      headings.forEach(h => {
        if(!h.id){
          const slug = h.textContent.trim()
            .toLowerCase().replace(/[^\w가-힣]+/g,'-').replace(/-+/g,'-').replace(/^-|-$/g,'');
          h.id = slug || ('h-' + Math.random().toString(36).slice(2,8));
        }
      });

      // 목차 DOM
      const toc = document.createElement('nav');
      toc.className = 'gh-toc';
      const title = document.createElement('div');
      title.className = 'gh-toc-title';
      title.textContent = 'On this page';
      const list = document.createElement('ol');

      headings.forEach(h => {
        const li = document.createElement('li');
        if(h.tagName === 'H3') li.style.marginLeft = '0.6rem';
        const a = document.createElement('a');
        a.href = '#' + h.id;
        a.textContent = h.textContent.trim();
        li.appendChild(a); list.appendChild(li);
      });

      toc.appendChild(title); toc.appendChild(list);
      // 본문 최상단에 삽입
      content.insertBefore(toc, content.firstElementChild);
    }
  }

  /* === 3) 리딩 프로그레스 바 === */
  const bar = document.createElement('div');
  bar.id = 'reading-progress';
  document.body.appendChild(bar);

  function updateProgress(){
    const target = document.querySelector('.gh-content');
    if(!target) return;
    const rect = target.getBoundingClientRect();
    const start = window.scrollY + rect.top;
    const end = start + target.offsetHeight - window.innerHeight;
    const ratio = Math.max(0, Math.min(1, (window.scrollY - start) / Math.max(1, end - start)));
    bar.style.width = (ratio * 100).toFixed(2) + '%';
  }
  window.addEventListener('scroll', updateProgress, {passive: true});
  window.addEventListener('resize', updateProgress);
  updateProgress();

  /* === 4) 코드블록 복사 버튼 === */
  document.querySelectorAll('pre > code').forEach(code => {
    const pre = code.parentElement;
    const btn = document.createElement('button');
    btn.className = 'gh-copy'; btn.type = 'button';
    btn.textContent = 'Copy';
    btn.addEventListener('click', async () => {
      try {
        await navigator.clipboard.writeText(code.innerText);
        const old = btn.textContent; btn.textContent = 'Copied'; setTimeout(()=>btn.textContent=old, 1200);
      } catch(e){
        // fallback
        const range = document.createRange(); range.selectNodeContents(code);
        const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);
        document.execCommand('copy'); sel.removeAllRanges();
        const old = btn.textContent; btn.textContent = 'Copied'; setTimeout(()=>btn.textContent=old, 1200);
      }
    });
    pre.appendChild(btn);
  });
})();
</script>

적용 팁(실무 감각)

  • 충돌 최소화 규칙: 전부 .post-template 프리픽스로 묶었고, 테마 업데이트에도 영향권은 Post 화면으로 제한된다.
  • 색상 일치: --ghost-accent-color를 자동 참조한다(브랜드 설정과 동기화). 필요하면 상단의 --accent만 변경.
  • 목차 헤딩 범위: 기본 H2/H3만 스캔한다. 더 깊게 원하면 쿼리셀렉터에 h4를 추가.
  • 퍼포먼스: 스크롤 핸들러는 가벼운 계산만 한다. 이미지가 매우 많다면 updateProgress()requestAnimationFrame 래핑 정도만 더하면 충분.

왜 이 4가지인가(디자인 관점)

  • 가독 리듬은 체류시간·이탈률을 바로 건드리는 1순위. 폰트 교체보다 행간/열폭/간격이 먼저다.
  • TOC는 긴 글 탐색의 “점프 포인트”를 만든다. 스티키로 두면 사이드바처럼 작동. Casper가 쓰는 .gh-content 기준이라 튼튼하다. (유령)
  • 프로그레스 바는 스크롤 의식화 → 완독률에 직접 영향. 매우 가볍고 시각적 간섭이 적다.
  • 미디어·코드 보강은 전문성 신호. 이미지와 코드가 깔끔하면 신뢰도가 바로 올라간다.

참고 소스

  • Casper 테마 구조/파일(README, post.hbs 등), 최신 릴리스 레일(5.8.1 기준). (GitHub)
  • Ghost 공식 튜토리얼(TOC: .gh-content, .gh-toc 전제). (유령)
  • Code Injection으로 레이아웃/스타일 보정 사례(포스트 헤더/높이 등). (Ghost Forum)

원하면 네 상황(폰트/컬러/여백 선호)에 맞춰 위 번들을 **“토글 가능한 옵션 세트”**로 쪼개 드릴 수 있다. 지금은 한 방에 체감을 내는 구성이니, 우선 깔고 체감부터 확인해.