
# Customization

The bundled example defaults live in `website/_config.yml`. Host repositories should put overrides in `.github/jekyll-obsidian.yml`, generated by `website/bin/integrate`, so `website/` remains replaceable. The effective public configuration stays small enough to review in one place:

```yaml
title: My Site
description: Built from Markdown
lang: en
url: ""
baseurl: ""

website:
  # jekyll-obsidian:managed-start
  source: docs
  theme: minimal
  # jekyll-obsidian:managed-end
  syntax_profile: ofm@1
  repository: owner/repository
  edit_branch: main
  content:
    publish_by_default: []
    default_type: doc
    directories:
      post: []
      doc: []
  features: {}
  contacts:
    - label: GitHub
      url: https://github.com/owner
    - label: Email
      url: mailto:hello@example.com
```

`website.source` is relative to the host repository root. The bundled defaults use `source: website/docs`; a generated host override normally uses `source: docs` or another directory outside `website/`.

The canonical commands merge `website/_config.yml`, then `.github/jekyll-obsidian.yml`, then temporary command-line or Pages values. The integration command owns only the marked `source` and `theme` lines in the host file; titles, repository links, content classification, and feature overrides remain editable around that block. `bin/build` pins the Jekyll source, implementation directories, caches, destination, and safety settings to `website/`. Direct `jekyll` commands do not load the host overlay or these ownership safeguards and are not a supported entrypoint.

`website.theme` selects `minimal` or `docs`. `website.syntax_profile` pins the Markdown contract and currently accepts only `ofm@1`. `website.edit_branch` selects the branch used by Edit and other host source links; it defaults to `main`.

## Publication defaults

`website.content.publish_by_default` is an array of directories relative to `website.source`. Each entry selects Markdown files in that directory and its descendants. The default is an empty array, which keeps `publish: true` as the required opt-in. Use `.` when every Markdown file in the content tree should publish unless it opts out:

```yaml
website:
  content:
    publish_by_default:
      - .
```

An explicit YAML boolean always wins. `publish: false` excludes one note from a selected directory, while `publish: true` includes one note outside those directories. Publication directories do not classify content; `default_type`, `directories.post`, `directories.doc`, and a note's `content_type` still decide whether a published note is a page, post, or document. Attachments remain private unless a published note, its `image` property, or its transclusion closure references them.

## Site identity

`title`, `description`, and `lang` feed the shell, metadata, Atom, and accessibility labels. Set `website.repository` to an `owner/repository` pair to show the Edit link. If it is blank, the build checks `GITHUB_REPOSITORY` and the local `origin` remote. The action stays hidden when no repository can be identified. This host repository setting never changes the Built by Jekyll Obsidian footer link, which always identifies the official project at `https://github.com/wowfun/jekyll-obsidian`.

Minimal Home renders the public root directory index, followed by the six most recent posts as editorial rows. The index is `index.md`, or the exact sibling `README.md` when no physical `index.md` exists. A selected directory index without an authored level-one heading does not render a substitute heading from its filename or frontmatter title; the resolved title remains available to metadata and navigation. A row with an image places a 16:9 thumbnail beside its summary on wide screens and above it on narrow screens; a row without an image uses the full width with no placeholder. Each summary can also show `subtitle`, a body excerpt, authors, and its publication date. The Blog link opens the complete reverse-chronological list at `/blog/`.

Add `website.contacts` to place contact links after the recent-post section. Each entry requires a short `label` and an `https:`, `mailto:`, or `tel:` URL; use an empty list or omit it when the base configuration does not define contacts. Email, phone, GitHub, LinkedIn, X/Twitter, Mastodon, Bluesky, Instagram, YouTube, Telegram, RSS, and Website links receive an accessible icon automatically. Other providers keep their label as text, so the configuration stays provider-neutral and needs no icon field.

## Site themes

Each build selects one complete presentation preset. `minimal` supports a Home page, Blog, Docs, and custom sections in one general-purpose shell. `docs` provides a focused hierarchical handbook. Search, wiki-link previews, page outlines, note relations, and the interactive graph are shared capabilities in both themes. A command-line override is useful for comparing the same content without editing configuration:

```sh
website/bin/dev --theme docs
website/bin/build --theme minimal --url https://example.test --baseurl "" --destination _site
```

The build writes the named destination below `website/`, so `_site` becomes `website/_site`. The supported theme identifiers are `minimal` and `docs`.

Feature keys omitted from `website.features` inherit the theme defaults. Explicit YAML booleans can override `search`, `tags`, `feed`, `graph`, `relations`, `previews`, and `outline`.

| Feature | Minimal | Docs |
| --- | --- | --- |
| `search`, `previews`, `outline`, `relations`, `graph` | on | on |
| `tags`, `feed` | on | off |

Set any shared feature to the YAML boolean `false` to remove it. For example:

```yaml
website:
  features:
    graph: false
    previews: false
```

## Minimal navigation

Minimal begins with Home, Blog, Docs, and Portfolio, ordered at `0`, `10`, `20`, and `30`. Blog and Docs disappear automatically when the content tree has no public post or documentation page. Portfolio uses the source-relative `portfolio` folder by default and appears when that folder contains at least one visible published project. An empty or unpublished folder adds no tab. Override built-in labels, order, visibility, or the Portfolio path under `website.navigation`:

```yaml
website:
  theme: minimal
  navigation:
    home:
      label: Home
      order: 0
      visible: true
    blog:
      label: Writing
      order: 10
      visible: true
    docs:
      label: Handbook
      order: 20
      visible: true
    portfolio:
      path: work
      label: Work
      order: 30
      visible: true
```

The Portfolio path is relative to `website.source`; an explicit path replaces the default `portfolio` path. Published Markdown descendants become Portfolio pages, so an explicit `content_type` within that path must be `page`. A public directory index keeps its authored introduction above the project grid. Use `<path>/index.md`, or `<path>/README.md` when no physical index exists. When the selected index is not public, the compiler generates the Portfolio index at that route. The selected index itself is not a project. Projects with `pinned: true` appear first; each group is then ordered by `nav_order`, title, and path. Set `nav_exclude: true` on a project to omit its card, or set `website.navigation.portfolio.visible: false` to hide only the tab while keeping the Portfolio index and project pages public.

Each project card uses its `image`, title, and `description`, falling back to a preview from the body when no description is present. Local GIF, WebP, AVIF, and APNG files are copied byte for byte and rendered with `<img>`, which preserves animation. The compiler does not transcode them or generate thumbnails.

A project can replace its local body with a public GitHub Markdown file by setting `github_markdown`. The local wrapper continues to own card metadata and its route, while the resolved remote body supplies the detail page, outline, Search text, preview, and Markdown endpoint. See [[Portfolio|Portfolio]] for the accepted URL and mapping forms, empty-body rule, branch refresh behavior, content limits, relative URLs, localization, and security boundary.

## Custom tabs

Minimal and Docs use the same custom-tab contract. Declare a tab on a published folder index, either `index.md` or its eligible `README.md` fallback. That note remains the canonical tab homepage, and its authored body appears above the generated member cards:

```yaml
---
publish: true
content_type: page
tab:
  id: favorites
  label: Favorites
  order: 40
  topics:
    - favorite
    - reference
---
```

`id` is required and must be a lowercase ASCII identifier such as `favorites` or `team-work`; `home`, `blog`, `docs`, and `portfolio` are reserved. The label defaults to the localized index title, and order defaults to `100`. A custom tab is always visible. Its root must be a visible `content_type: page` folder index and cannot sit inside the active Portfolio path.

Members are the union of three sources: every published descendant of the tab folder, any published note that names the tab in `tabs`, and any published note whose `tags` or `categories` match one of the tab's `topics`. Multiple topics use OR matching after Unicode normalization and case folding. Add a page outside the folder without changing its route:

```yaml
---
publish: true
tabs:
  - favorites
---
```

Membership is additional: a Blog post remains active under Blog, a documentation page remains active under Docs, and a Portfolio page remains active under Portfolio. Only ordinary page descendants use the custom tab as their active navigation item; the deepest nested custom root wins. Cards are de-duplicated and sorted by `pinned`, then `nav_order`, localized title, and source path. `nav_exclude: true` removes a folder or topic match and cannot be combined with `tabs`. An empty topic result is valid and leaves only the authored index body.

Duplicate IDs, labels, or destinations, unknown `tabs` references, invalid roots, and reserved conflicts fail the build. Notes keep one canonical URL even when several tabs include them; custom tabs never create redirects or duplicate Search, feed, or sitemap entries.

Tabs that do not fit the desktop header move into an accessible More menu. The mobile Browse sheet and the Search dialog's quick navigation use the same ordered destinations and active state. Search narrows those quick links by their configured labels while it queries note content. Without JavaScript, the header links remain visible and wrap naturally. Opening Docs from Minimal keeps the Minimal site shell while adding the handbook tree, page outline, and previous or next links.

## Graph and wiki-link previews

A published note places its one-hop graph above Outline and Relations in the right-hand context rail only when it links to, embeds, is linked from, or is embedded by another public note in the current language partition. The graph contains the current note and those direct neighbours. Isolated notes and notes with only self-links omit the local graph, while the complete graph still contains their nodes. Node area grows with its degree in the complete public graph.

Use the graph's left button to open the complete graph and its right button to enlarge the current note's local graph. In either view, scroll the mouse wheel over the canvas to zoom around the pointer, drag empty canvas space to pan, and drag a neighbouring node to reposition it; the current node stays fixed at the visual centre. Click a node to visit that note, or focus it and press Enter or Space. The complete graph JSON loads only when its dialog is opened and always contains every public node and relation. To keep the page responsive, the SVG viewer declines to render complete graphs above 250 nodes or 1,000 relations and directs readers to local graphs or search instead. There is no generated `/graph/` page or navigation tab, so a published note may use that route.

Rest the pointer on a wiki link for 0.3 seconds, or keyboard-focus it, to open its reading preview. Leaving before the pointer delay cancels the preview; keyboard focus remains immediate. Catalog metadata appears first, followed by a compact, independently scrolling view of the target note's sanitized body. The preview title opens the target note; links inside the body remain plain text. Touch taps on the original wiki link continue directly to the target page. Set `previews: false` to disable this behavior.

## GitHub Discussions comments

Both themes can attach a GitHub Discussion to each `content_type: post` through [Giscus](https://giscus.app/). Comments are disabled when `website.comments` is absent. When the mapping is present and omits `enabled`, Minimal enables comments while Docs keeps them disabled. To use comments, first prepare a public GitHub repository:

1. Enable **Settings → General → Features → Discussions**.
2. Create a `Blog comments` category. The Announcement format is recommended because maintainers and Giscus can create discussions there while visitors can still reply.
3. Install the [Giscus GitHub App](https://github.com/apps/giscus) for that repository.
4. Enter the repository and category at [giscus.app](https://giscus.app/) and copy their generated IDs.

Then add the values to the host configuration:

```yaml
website:
  theme: minimal
  repository: owner/site
  comments:
    # Optional. Omit this to reuse website.repository.
    repository: owner/community
    repository_id: R_kgDOxxxxxxxx
    category: Blog comments
    category_id: DIC_kwDOxxxxxxxx
```

Add `enabled: true` inside `comments` when the active theme is Docs; add `enabled: false` to keep a configured Minimal site dormant. The comments repository may be the publication repository or a separate public community repository. The build never checks whether Discussions is enabled or the Giscus App is installed. Missing provider IDs produce a warning and a non-interactive fallback instead of failing the build. Add a `giscus.json` file to the comments repository when you want to restrict embedding to the production site origin.

Once enabled, every `content_type: post` has comments in any theme. Disable one article with a YAML boolean:

```yaml
---
publish: true
content_type: post
comments: false
---
```

Threads use a strict, route-independent term derived from the note path. Changing a domain, `baseurl`, or permalink keeps the thread; moving or renaming the source note creates a new identity. Reactions are enabled, the input appears above existing comments, and Giscus lazily loads its iframe near the comments area. The Giscus client itself is still requested when a published comment page initializes.

Local development does not connect to Giscus. It shows a publication-only notice and a normal link to the repository's Discussions page. Published and CI builds load the widget, synchronize it with the site's light or dark scheme, and retain the GitHub link when JavaScript or the external service is unavailable. Localized pages share the post's route-independent Discussion thread, while the Giscus interface follows each page locale when supported and otherwise falls back to English.

See [[Comments|Comments with GitHub Discussions]] for thread identity, origin restrictions, privacy boundaries, and troubleshooting.

## Analytics

Analytics is off when `website.analytics` is absent. A production site can select Cloudflare Web Analytics:

```yaml
website:
  analytics:
    provider: cloudflare
    token: SITE_TOKEN
```

Or it can select one Google Analytics 4 property:

```yaml
website:
  analytics:
    provider: google
    measurement_id: G-XXXXXXXXXX
```

Only one provider may be active. Local development and redirect pages do not load either client. `website/bin/integrate` preserves this mapping but does not create it. See [[Analytics|Analytics]] for provider setup, Docs navigation tracking, Content Security Policy sources, privacy differences, and troubleshooting.

## Localization

Both themes can opt into static localization by listing locales under `website.i18n`. The mapping enables localization by default for Docs and stays disabled by default for Minimal; set `enabled: true` there. The top-level `lang` is the default locale and must appear in the list when localization is enabled:

```yaml
lang: en

website:
  theme: docs
  i18n:
    locales:
      - en
      - zh-CN
```

Add `enabled: true` inside `i18n` for Minimal; add `enabled: false` to keep a configured Docs locale plan dormant. Keep the default language in the normal content tree. Put translated notes at the same relative path below `_translations/<locale>/`. A translation inherits the publication state of its public default-language note; set `publish: false` on the translation to show the existing default-language fallback instead. Every configured locale requires `_locale.yml` at its locale root; `name` is required, while `hreflang`, `dir`, and the closed `messages` catalog are optional. A missing or disabled translation does not fail the build. It keeps its localized URL, briefly shows a notice over the default-language content, and is excluded from search-engine indexing and the sitemap.

```yaml
name: 简体中文
hreflang: zh-Hans
dir: ltr
messages:
  search: 搜索
```

`dir` accepts only `ltr` or `rtl`. Message values must be strings and keys must belong to the closed built-in theme catalog; unknown keys fail the build. Omitted messages use the built-in English text. The locale list order is also the language-switcher order.

The default locale keeps existing URLs. Other locales use their configured tag as a prefix, such as `/zh-CN/docs/Getting%20Started/`. Navigation, search, theme system pages, language switching, SEO metadata, and locale assets stay partitioned for Minimal and Docs alike.

See [[Localization|Localization guide]] for locale manifests, translation authority, fallback pages, SEO behavior, and troubleshooting.

## Color and type

The themes share a narrow set of CSS custom properties. Light mode uses warm paper and surface tones; dark mode uses neutral black and gray. Blue marks links and focus, while terracotta marks relationships and secondary annotations.

Articles and controls use the platform sans-serif stack, with installed Noto Sans CJK or Source Han Sans preferred for CJK text. Code uses the platform monospace stack. The site does not download a webfont, so the exact face follows the reader's operating system while the typographic scale and spacing remain consistent.

Override tokens in your own stylesheet rather than editing hashed build output. Keep text and focus contrast above WCAG AA in both color schemes.

## Page properties

The compiler reserves these note properties for publication behavior and validates each one with a closed schema:

- `publish`, `title`, `subtitle`, `aliases`, `tags`, `author`, `categories`, and `description`
- `permalink`, `image`, and `cssclasses`
- `created` and `updated`
- `content_type`, `date`, `pinned`, `nav_order`, `nav_exclude`, `tab`, and `tabs`
- `comments`, `github_markdown`, and `related`

Other top-level properties may contain scalar values or flat scalar lists. Custom property names must be normalized, single-line text without leading or trailing whitespace. A complete wiki link in one of those values becomes an ordinary note relation when it is a YAML double-quoted string. Custom properties remain compiler metadata: their keys and raw values never flow into Liquid, HTML, feeds, or generated JSON.

```yaml
project: "[[portfolio/fauni-search|Fauni Search]]"
references:
  - background
  - "[[docs/development/architecture]]"
related:
  - "[[blog/launch-notes]]"
  - "[[docs/Getting Started|Start here]]"
```

`related` is the curated list variant. It accepts only a list of double-quoted wiki links, preserves the authored order, removes repeated targets, and shows the resolved pages as Recent-post-style cards at the bottom of the page. Broken related links fail a production build. Other custom-property links still join Direct links, Backlinks, and Graph data but do not create a second visible field table.

`aliases`, `tags`, `author`, `categories`, `cssclasses`, and `tabs` are string arrays; `subtitle` is a string. `publish`, `pinned`, `nav_exclude`, and `comments` use YAML booleans. `tab` is the closed mapping documented above. `github_markdown` accepts only the URL or mapping documented in [[Portfolio|Portfolio]], and only on a Portfolio project wrapper. Dates use ISO 8601. A note title comes from `title`, its first level-one heading, or its filename, in that order.

`updated` is optional and appears in page metadata only when the author supplies it; the compiler never infers an update date from Git. A post's publication time uses `date`, then `created`, then its first Git commit. Atom entries use explicit `updated` when present and otherwise use that publication time for posts. A non-post note without `updated` is omitted from the feed.

Minimal Home shows `subtitle` below the article title, then uses `description` or a compiler-generated body excerpt for the summary. When `author` is present, the summary footer lists its values. Both `author` and `categories` join `tags` in the Home Topics area. Their entries may be ordinary strings or wiki links to public notes:

```yaml
---
subtitle: Dreamers among programmers
author:
  - "[[People/Ada Lovelace|Ada]]"
  - Editorial team
categories:
  - "[[AI]]"
  - "[[Machine Learning|ML]]"
---
```

Wiki-link entries must be YAML double-quoted strings. Their visible label uses the alias after `|`, or the target note title when no alias is supplied. Every Home Topics capsule opens the matching Blog filter; a wiki-linked author shown in a post summary may additionally link directly to that public author page. On Minimal Blog, tags and categories appear together below each archive date and as filter capsules beside the publication date on the article page. Repeating the same name across `tags` and `categories` shows one topic. Unresolved wiki links produce a compiler warning and remain filterable text instead of leaking `[[...]]` into the site.

## Content and navigation

An explicit `content_type: post | doc | page` wins over directory defaults. Post publication dates use `date`, then `created`, then the first Git commit. A production build rejects a post with no deterministic date. On Minimal Home and Blog, posts with `pinned: true` appear before unpinned posts; both groups retain reverse-chronological order. Pinning affects card presentation only, not Atom feed chronology or the previous and next post sequence.

Docs navigation follows vault directories. `nav_order` sorts sibling documents and `nav_exclude: true` removes only that note link; children remain reachable. `index.md` is optional at every level. When no physical `index.md` exists, a published exact `README.md` becomes the folder index. An existing `index.md` reserves that role even when unpublished, leaving `README.md` as an ordinary note. A published root index always owns `/`, so a different `permalink` on that note is rejected. A folder without a public selected index links to its first visible child after `nav_order`, title, and path sorting. Minimal Home can render without a root index when posts exist; if neither exists, the root redirects to the first visible top-level navigation destination. Docs redirects an indexless root to its first navigation item. Every Docs page server-renders the complete documentation tree; JavaScript navigation preserves the shared shell while replacing only page-specific content and context. Search builds its index in a Web Worker. Local graphs are projected by the compiler; the browser fetches the complete graph only after its dialog opens.

See [[docs/development/architecture|Architecture]] before changing compiler or adapter seams.
