In this article, we'll take a deep dive into how Obsidian Publish renders Markdown files and resolves cross-links between pages. We'll break down the main components and classes involved, and look at the process step-by-step. We'll cover topics like Markdown rendering, resolving internal links, loading site data, and explain how the NavigationView component works. This article aims to provide a structural and technical overview of how Obsidian Publish works under the hood.

The Obsidian Publish website source code contains the following modules:

Main Publish Class

The Publish class initializes and manages the entire site. It loads the site info, options, and cache data. It contains properties for rendering Markdown content, navigation, search, graph view, and outline view.

It handles tasks like:

  • Navigating to Markdown files
  • Applying themes
  • Applying CSS styles
  • Registering Markdown post-processors
class Publish {
  constructor() {
    this.render = new MarkdownRenderer(this) 
  }

  navigate(filepath, subpath) {
    // Loads and renders the Markdown file  
  }

  onResize() {
    // Handles resize events
  }

  setTheme(theme) {
    // Applies a theme
  }

  applyCss(css) { 
    // Applies CSS styles
  }

  registerMarkdownPostProcessor(processor, sortOrder) {
    // Registers a post-processor for Markdown content  
  }
}

MarkdownRenderer Class

This class renders Markdown content, handles scrolling, load external embeds, etc.

class MarkdownRenderer {
  loadFile(filepath) {
    // Loads a Markdown file and renders it
  }

  onScroll() {
    // Handles scroll events
  }

  loadEmbed(src) {
    // Loads external embeds like images, audio, videos etc.  
  }
}

Rendering Markdown in Obsidian Publish works like this:

  1. The MarkdownRenderer loads a Markdown file using:
this.site.loadMarkdownFile(filepath)
  1. This returns the Markdown content as a string.
  2. The MarkdownRenderer then sets that content into the Markdown renderer instance using:
this.renderer.set(markdownContent)
  1. The Markdown renderer class then parses the Markdown and renders it into DOM nodes.

  2. After rendering, a series of post-processors are run on the DOM nodes to:

    • Apply client-side markdown extensions
    • Resolve internal links
    • Load embeds
    • Register event listeners
  3. Once post-processing is complete, the rendered DOM nodes are injected into the DOM.

  4. The MarkdownRenderer then handles scrolling, resize events, external embeds, etc. for that rendered Markdown content.

One of the most interesting part of obsidian is it's cross-links logics. Here is how it works when page render.

  1. When a Markdown file is loaded and rendered, post-processors run after initial parsing.
  2. One of these post-processors resolves internal links:
function resolveInternalLinks(el) {
  const links = el.findAll('a.internal-link');
  for (const link of links) {
    const href = link.getAttr("data-href");
    const { path, subpath } = parseURL(href);
    
    // Resolve the link destination
    const dest = site.cache.getLinkpathDest(path);
    
    // Set the resolved link href
    if (dest) {
      link.setAttr("href", site.getPublicHref(dest) + subpath);
    }
  }
}
  1. This uses the PublishSite.cache to resolve the link destination from the "linkpath" - which can be either the filepath or an alias.
  2. It then sets the final <a> href to the public URL of that destination page.
  3. Any unresolved internal links are marked with a class.
  4. When loading embeds from other pages, the same process is used to resolve the source filepaths.
  5. Headers are also resolved, so headings can link to the correct destination.
  6. If a resolved destination page does not exist, the link is marked as "unresolved".

So in summary, cross-links are resolved by:

  • Parsing the linkpath from the Markdown link
  • Using the PublishSite.cache to find the actual destination filepath
  • Generating the public URL for that destination
  • Setting the final <a> href to that URL
  • Marking any unresolved links
  • Doing the same for embeds and headers
  • Handling non-existent destinations

This allows links in your Markdown to just use "linkpath" references, and Obsidian Publish figures out the actual destination and public URL at render time.

PublishSite class

This class represents the actual site data, including options, cache, etc.

class PublishSite {
  loadCache() {
    // Loads the site cache data  
  }

  loadOptions() {
    // Loads the site options
  }

  getConfig(optionName) {
    // Gets a site option 
  }

  getInternalUrl(filepath) {
    // Gets the internal URL for a file  
  }
}

The site data contains all the information needed to render an Obsidian Publish site. It includes:

  • Site options: Things like the site name, logo, themes, navigation settings, etc. This is stored in the options property of the PublishSite class.
  • Site cache: The actual Markdown files that make up the site, along with frontmatter and other metadata. This is stored in the cache property of the PublishSite class.
  • Publish handles: The actual site handle, used to authenticate requests and fetch site data.
  • Status: Whether the site is active, degraded, etc. This is stored in the status property of the PublishSite class.

The Publish code fetches this data when initializing the site. Then it uses that data to:

  • Configure the site based on the options
  • Render Markdown pages from the cache
  • Fetch internal URLs referencing files in the cache
  • Apply the status (e.g. show a banner for degraded sites)

So the site data contains all the information Obsidian Publish needs to fully render and configure the site. The Publish code handles fetching this data when initializing and uses it throughout the rendering process.

The most important parts of site data are:

  • Site options - Used to configure the site
  • Site cache - Contains the actual Markdown files that make up the site

The NavigationView component is used to render the site navigation. It shows a tree-based view of the site files and folders. Its main purposes are:

  • It renders the navigation tree based on the site structure. It uses filepaths to determine the hierarchy and shows folders and files accordingly.
  • It handles clicks on navigation items. When a navigation item is clicked, it navigates to that file using the Publish.navigate() method.
  • It handles activating the currently active navigation item. When the Publish code navigates to a file, NavigationView updates the active item in the navigation.
  • It can render two types of navigation items: FolderItems and FileItems. FolderItems render folders and can be collapsed/expanded, while FileItems render files and navigate directly when clicked.
  • It loads the navigation order and hidden items from the site config, and renders the navigation accordingly.

So in summary, its main purpose is to provide the site navigation by:

  • Rendering the site structure as a tree
  • Handling clicks to navigate
  • Highlighting the active item
  • Supporting folders and files
  • Following the configured navigation order and hidden items
    This gives the user an easy way to browse and navigate the site using the left navigation column.

Other Components

The code also contains components like:

  • GraphView
  • SearchView
  • OutlineView

Which are used to render the corresponding UI parts on the site.