Diving deep

source: Theme Development

Every new mkdocs project (which will ofcourse be a website) in our workflow will involve two directories:

vector@ArchLinuxVS ~ $ tree -L 1 /home/vector/development/mkdocs
/home/vector/development/mkdocs
├── mkdocs-material
└── site

...where mkdocs-material is the result of git clone https://github.com/squidfunk/mkdocs-material and site is the site we made using mkdocs new site.

cd mkdocs-material
python -m venv venv
source venv/bin/activate
pip install mkdocs-material
pip install -e ".[recommended]"
pip install nodeenv
nodeenv -p -n lts
npm install

Now I edited mkdocs/mkdocs-material/tools/build/_/index.ts so that any modication made to templates in src directory will lead to a recompiling and output produced in mkdocs/mkdocs-material/material/templates.

mkdocs/mkdocs-material/tools/build/_/index.ts
import * as chokidar from "chokidar"
import * as fs from "fs/promises"
import * as path from "path"
import {
  EMPTY,
  Observable,
  filter,
  from,
  fromEvent,
  identity,
  catchError,
  defer,
  map,
  mergeWith,
  of,
  switchMap,
  tap
} from "rxjs"
import glob from "tiny-glob"

/* ----------------------------------------------------------------------------
 * Helper types
 * ------------------------------------------------------------------------- */

/**
 * Resolve options
 */
interface ResolveOptions {
  cwd: string                          /* Working directory */
  watch?: boolean                      /* Watch mode */
  dot?: boolean                        /* Hidden files or directories */
}

/**
 * Watch options
 */
interface WatchOptions {
  cwd: string                          /* Working directory */
}

/* ----------------------------------------------------------------------------
 * Data
 * ------------------------------------------------------------------------- */

/**
 * Base directory for compiled files
 */
export const base = "material"

/**
 * Cache to omit redundant writes
 */
export const cache = new Map<string, string>()

/* ----------------------------------------------------------------------------
 * Helper Ffunctions
 * ------------------------------------------------------------------------- */

/**
 * Return the current time
 *
 * @returns Time
 */
function now() {
  const date = new Date()
  return [
    `${date.getHours()}`.padStart(2, "0"),
    `${date.getMinutes()}`.padStart(2, "0"),
    `${date.getSeconds()}`.padStart(2, "0")
  ]
    .join(":")
}

/* ----------------------------------------------------------------------------
 * Functions
 * ------------------------------------------------------------------------- */

/**
 * Resolve a pattern
 *
 * @param pattern - Pattern
 * @param options - Options
 *
 * @returns File observable
 */
export function resolve(
  pattern: string, options?: ResolveOptions
): Observable<string> {
  return from(glob(pattern, { dot: true, ...options }))
    .pipe(
      catchError(() => EMPTY),
      switchMap(files => from(files).pipe(

        /* Start file watcher */
        options?.watch
          ? mergeWith(watch(files, options))
          : identity
      )),

      /* Build overrides */
      !process.argv.includes("--all")
        ? filter(file => !file.startsWith(`.overrides${path.sep}`))
        : identity,
    )
}

/**
 * Watch all given files
 *
 * @param files - Files
 * @param options - Options
 *
 * @returns File observable
 */
// export function watch(
//   files: string[], options: WatchOptions
// ): Observable<string> {
//   return fromEvent(
//     chokidar.watch(files, options),
//     "change", file => file // see https://t.ly/dli_k
//   ) as Observable<string>
// }

export function watch(
  patterns: string[], options: WatchOptions
): Observable<string> {
  // If the patterns look like file paths (no wildcards),
  // watch the containing directories instead
  const watchTargets = patterns.map(p => {
    if (!p.includes('*') && !p.includes('?')) {
      // Get the directory of the file
      return path.dirname(p);
    }
    return p;
  });

  return fromEvent(
    chokidar.watch(watchTargets, {
      ...options,
      ignoreInitial: true  // Don't emit events for initial scan
    }),
    "all", // Watch all events, not just "change"
    (eventName, file) => file // Return the file path
  ).pipe(
    filter(file => patterns.some(pattern => {
      // If the pattern is a direct file path, match exactly
      if (!pattern.includes('*') && !pattern.includes('?')) {
        return file === pattern;
      }
      // Otherwise use a simple glob matcher
      return file.endsWith(path.extname(pattern));
    }))
  ) as Observable<string>;
}

/* ------------------------------------------------------------------------- */

/**
 * Recursively create the given directory
 *
 * @param directory - Directory
 *
 * @returns Directory observable
 */
export function mkdir(directory: string): Observable<string> {
  return defer(() => fs.mkdir(directory, { recursive: true }))
    .pipe(
      map(() => directory)
    )
}

/**
 * Read a file
 *
 * @param file - File
 *
 * @returns File data observable
 */
export function read(file: string): Observable<string> {
  return defer(() => fs.readFile(file, "utf8"))
}


/**
 * Write a file, but only if the contents changed
 *
 * @param file - File
 * @param data - File data
 *
 * @returns File observable
 */
export function write(file: string, data: string): Observable<string> {
  let contents = cache.get(file)
  if (contents === data) {
    return of(file)
  } else {
    cache.set(file, data)
    return defer(() => fs.writeFile(file, data))
      .pipe(
        map(() => file),
        process.argv.includes("--verbose")
          ? tap(file => console.log(`${now()} + ${file}`))
          : identity
      )
  }
}

Now do:

npm start

The template wills will be generated in mkdocs-material/material/templates folder.

templates $ ls
404.html  base.html  blog-post.html  __init__.py  mkdocs_theme.yml  __pycache__
assets    blog.html  fragments       main.html    partials          redirect.html

Take note of the overrides directory in src.

mkdocs-material/src/overrides

The contents of this directory shows a sampe site implementation using mkdocs material.

```vector@ArchLinuxVS ~/development/mkdocs/mkdocs-material/src $ tree overrides overrides ├── assets │   ├── javascripts │   │   ├── components │   │   │   ├── _ │   │   │   │   └── index.ts │   │   │   ├── iconsearch │   │   │   │   ├── _ │   │   │   │   │   └── index.ts │   │   │   │   ├── index.ts │   │   │   │   ├── query │   │   │   │   │   └── index.ts │   │   │   │   └── result │   │   │   │   └── index.ts │   │   │   ├── index.ts │   │   │   └── sponsorship │   │   │   └── index.ts │   │   ├── custom.ts │   │   ├── integrations │   │   │   ├── analytics │   │   │   │   └── index.ts │   │   │   └── index.ts │   │   └── templates │   │   ├── iconsearch │   │   │   └── index.tsx │   │   ├── index.ts │   │   └── sponsorship │   │   └── index.tsx │   └── stylesheets │   ├── custom │   │   ├── layout │   │   │   ├── _banner.scss │   │   │   ├── _hero.scss │   │   │   ├── _iconsearch.scss │   │   │   └── _sponsorship.scss │   │   └── _typeset.scss │   └── custom.scss ├── home.html ├── hooks │   ├── shortcodes.py │   ├── translations.html │   └── translations.py └── main.html

Now create a symbolic link to the `mkdocs/material/templates` in the new site you are
developing
cd site ln -s /home/vector/development/mkdocs/mkdocs-material/material/templates /home/vector/development/mkdocs/sreema/overrides
```yml title="mkdocs.yml"
site_name: My Docs
theme:
  name: material
  custom_dir: overrides

mkdocs serve --watch-theme -a 127.0.0.1:8005

Its probably not best practice to use the templates folder like this. But I'm at the point where I value control and I wannt see for myself if I can make really customized sites with mkdocs.

Now let's develop the site.


Comments