타임라인용 RecentUpdatesTable 구성

변경 요약

  • 문서 전체의 Added(작성) / Modified(수정) 이력을 한눈에 볼 수 있는 표 컴포넌트 추가.
  • 슬러그가 timeline인 페이지에서만 대용량 표(상한 무제한)를 렌더링하고, 그 외 페이지에는 노출하지 않음.

변경된 파일

  • quartz/components/RecentUpdatesTable.tsx
  • quartz/components/styles/recentUpdatesTable.scss
  • quartz/components/index.ts
  • quartz.layout.ts

주요 구현 포인트

  • 정렬: 수정일 내림차순 + 제목 알파벳 보조 정렬.
  • 열 구성: Title / Path / Added / Modified(반응형 테이블 래퍼 포함).
  • Path 표기: 슬러그에서 파일명을 제외한 경로만 표시(없으면 em dash).
  • 조건부 렌더링: page.fileData.slug === 'timeline'일 때만 표를 렌더.
  • 접근성: 각 셀에 data-label을 부여해 모바일에서 헤더-셀 매핑이 유지되며, 값이 없을 때는 aria-hidden="true"로 시각적 대시를 출력.
  • 반응형: .table-wrapper { overflow-x: auto; }로 좁은 화면에서 가로 스크롤 허용.

왜 변경했는가

  • “최근 글” 위젯을 보완해, 사이트 전체의 생성/수정 흐름을 투명하게 공개하려는 목적.
  • 글감 관리/운영 점검 시 유용한 시스템 뷰 제공.

핵심 코드

아래는 실제 반영된 코드 중 핵심 발췌본입니다. 전체 구현은 각 파일에서 확인하세요.

1) RecentUpdatesTable 컴포넌트

quartz/components/RecentUpdatesTable.tsx 의 옵션, 경로 정규화, 테이블 구조입니다.

// quartz/components/RecentUpdatesTable.tsx:1
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { QuartzPluginData } from "../plugins/vfile"
import { byDateAndAlphabetical } from "./PageList"
import { GlobalConfiguration } from "../cfg"
import { i18n } from "../i18n"
import { classNames } from "../util/lang"
import { resolveRelative } from "../util/path"
import { Date } from "./Date"
import style from "./styles/recentUpdatesTable.scss"
 
interface Options {
  title?: string
  limit: number
  filter: (file: QuartzPluginData) => boolean
  sort: (a: QuartzPluginData, b: QuartzPluginData) => number
  showPath: boolean
}
 
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
  title: i18n(cfg.locale).components.recentNotes.title,
  limit: 10,
  filter: () => true,
  sort: byDateAndAlphabetical(cfg),
  showPath: true,
})
 
function formatPath(slug?: string): string {
  if (!slug) {
    return ""
  }
  const cleaned = slug.replace(/\\/g, "/")
  const trimmed = cleaned.replace(/\/index$/, "")
  const withoutTrailingSlash = trimmed.replace(/\/$/, "")
  const segments = withoutTrailingSlash.split("/").filter(Boolean)
  if (segments.length <= 1) {
    return ""
  }
  segments.pop()
  return segments.join("/")
}
 
export default ((userOpts?: Partial<Options>) => {
  const RecentUpdatesTable: QuartzComponent = ({
    allFiles,
    fileData,
    displayClass,
    cfg,
  }: QuartzComponentProps) => {
    const opts = { ...defaultOptions(cfg), ...userOpts }
    const pages = allFiles.filter(opts.filter).sort(opts.sort).slice(0, opts.limit)
 
    return (
      <div class={classNames(displayClass, "recent-updates-table")}>
        {opts.title && <h3>{opts.title}</h3>}
        <div class="table-wrapper">
          <table>
            <thead>
              <tr>
                <th scope="col">Title</th>
                {opts.showPath && <th scope="col">Path</th>}
                <th scope="col">Added</th>
                <th scope="col">Modified</th>
              </tr>
            </thead>
            <tbody>
              {pages.map((page) => {
                const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
                const created = page.dates?.created
                const modified = page.dates?.modified
                const pathLabel = opts.showPath ? formatPath(page.slug) : ""
 
                return (
                  <tr key={page.slug}>
                    <td data-label="Title">
                      <a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
                        {title}
                      </a>
                    </td>
                    {opts.showPath && (
                      <td class="path-cell" data-label="Path">
                        {pathLabel ? <span>{pathLabel}</span> : <span aria-hidden="true">—</span>}
                      </td>
                    )}
                    <td class="date-cell" data-label="Added">
                      {created ? (
                        <Date date={created} locale={cfg.locale} />
                      ) : (
                        <span aria-hidden="true">—</span>
                      )}
                    </td>
                    <td class="date-cell" data-label="Modified">
                      {modified ? (
                        <Date date={modified} locale={cfg.locale} />
                      ) : created ? (
                        <Date date={created} locale={cfg.locale} />
                      ) : (
                        <span aria-hidden="true">—</span>
                      )}
                    </td>
                  </tr>
                )
              })}
            </tbody>
          </table>
        </div>
      </div>
    )
  }
 
  RecentUpdatesTable.css = style
  return RecentUpdatesTable
}) satisfies QuartzComponentConstructor

설정 포인트

  • limit: 기본 10. 타임라인 페이지에서는 사실상 무제한(큰 값)으로 재설정.
  • sort: 기본값은 byDateAndAlphabetical, 타임라인에서는 sortByModified로 명시적 최신 수정일 우선.
  • formatPath: 파일명을 제외한 디렉토리 경로만 표시해 Path 칼럼의 밀도를 낮춤.

2) 레이아웃 연결 및 정렬 기준

quartz.layout.ts에서 정렬 기준, 조건부 렌더링, 이중 날짜 표기를 설정했습니다.

// quartz.layout.ts:1
import { PageLayout, SharedLayout } from "./quartz/cfg"
import * as Component from "./quartz/components"
import { QuartzPluginData } from "./quartz/plugins/vfile"
 
const sortByModified: (a: QuartzPluginData, b: QuartzPluginData) => number = (a, b) => {
  const aModified = a.dates?.modified?.getTime?.() ?? 0
  const bModified = b.dates?.modified?.getTime?.() ?? 0
 
  if (aModified === bModified) {
    const aTitle = a.frontmatter?.title?.toLowerCase?.() ?? ""
    const bTitle = b.frontmatter?.title?.toLowerCase?.() ?? ""
    return aTitle.localeCompare(bTitle)
  }
 
  return bModified - aModified
}
 
export const sharedPageComponents: SharedLayout = {
  head: Component.Head(),
  header: [],
  afterBody: [
    Component.ConditionalRender({
      component: Component.RecentUpdatesTable({
        title: "All Content",
        limit: 999999999,
        sort: sortByModified,
      }),
      condition: (page) => page.fileData.slug === "timeline",
    }),
    // ... 생략 ...
    Component.RecentNotes({ title: "Recently Added", limit: 5, linkToMore: "timeline" }),
  ],
  // ... 생략 ...
}
 
export const defaultContentPageLayout: PageLayout = {
  beforeBody: [
    Component.Breadcrumbs(),
    Component.ContentMeta({
      dateTypes: ["modified", "created"],
      dateLabels: { created: "Created", modified: "Updated" },
    }),
    Component.TagList(),
  ],
  // ... 생략 ...
}

핵심 요약

  • sortByModified: 최신 수정 → 동률 시 제목 알파벳 정렬.
  • ConditionalRender: 슬러그가 timeline일 때만 표 렌더링.
  • ContentMeta: 본문/목록 레이아웃 모두에서 UpdatedCreated를 함께 표시.

3) 컴포넌트 등록

quartz/components/index.ts에 컴포넌트를 등록해 레이아웃에서 사용할 수 있게 했습니다.

// quartz/components/index.ts:1
import RecentUpdatesTable from "./RecentUpdatesTable"
 
export {
  // ... 생략 ...
  RecentUpdatesTable,
  // ... 생략 ...
}

4) 스타일

모바일 스크롤, 날짜/경로 셀 처리, 테이블 타이포그래피를 정의했습니다.

// quartz/components/styles/recentUpdatesTable.scss:1
.recent-updates-table {
  margin: 0.5rem 0 1rem;
 
  & > h3 {
    margin: 0.5rem 0;
    font-size: 1rem;
  }
 
  .table-wrapper {
    overflow-x: auto;
  }
 
  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 1rem;
  }
 
  th,
  td {
    padding: 0.5rem 0.5rem;
    text-align: left;
    vertical-align: middle;
  }
 
  thead {
    border-bottom: 1px solid var(--lightgray);
  }
 
  tbody {
    tr { border-bottom: 1px solid var(--lightgray); }
    .date-cell { white-space: nowrap; color: var(--secondary); }
    .path-cell { font-size: 1rem; color: var(--secondary); word-break: break-all; }
  }
}

운영 체크리스트

  • 문서가 많을 경우 테이블이 길어질 수 있으므로, 모바일 가독성을 위해 폭/스크롤 동작을 함께 점검.
  • RecentNotes의 더보기 링크를 timeline으로 연결하여 탐색 흐름을 자연스럽게 유지.
    • quartz.layout.tslinkToMore: "timeline" 설정 확인.
  • 타임라인 페이지가 아닌 곳에서 표가 보이지 않는지 재확인(ConditionalRender 조건 검증).
  • Updated/Created 이중 표기가 의도대로 출력되는지 대표 문서 2~3개로 확인.

관련 커밋

  • 3ad7489 feat(layout): surface timeline updates table and dual meta dates