# Quick Switcher: Allow searching for \`title\` property, similar to \`aliases\`

**URL:** <https://forum.obsidian.md/t/quick-switcher-allow-searching-for-title-property-similar-to-aliases/112609>\
**Category:** Help\
**Tags:** quick-switcher, properties\
**Created:** [March 24, 2026, 2:43pm UTC](https://forum.obsidian.md/t/quick-switcher-allow-searching-for-title-property-similar-to-aliases/112609 "2026-03-24T14:43:40Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![LucasOe](https://forum.obsidian.md/user_avatar/forum.obsidian.md/lucasoe/32/78148_2.png) [@LucasOe](https://forum.obsidian.md/u/LucasOe)\
**Post date:** [March 24, 2026, 2:43pm UTC](https://forum.obsidian.md/t/quick-switcher-allow-searching-for-title-property-similar-to-aliases/112609/1 "2026-03-24T14:43:40Z")

</div>

### Use case or problem

The Quick Switcher core plugins allows to search for notes by their `aliases` frontmatter property, but not by `title`.

### Proposed solution

The Quick Switcher should also show notes with a matching `title` property, just like it works with aliases.

---

<div class="post-metadata">

**Author:** ![WhiteNoise](https://forum.obsidian.md/user_avatar/forum.obsidian.md/whitenoise/32/11_2.png) [@WhiteNoise](https://forum.obsidian.md/u/WhiteNoise)\
**Post date:** [March 24, 2026, 2:47pm UTC](https://forum.obsidian.md/t/quick-switcher-allow-searching-for-title-property-similar-to-aliases/112609/2 "2026-03-24T14:47:04Z")

</div>

This doesn’t happen primarily because the “title” property is not Obsidian Native (like aliases, tags, cssclasses), it’s something that some users add.

---

<div class="post-metadata">

**Author:** ![WhiteNoise](https://forum.obsidian.md/user_avatar/forum.obsidian.md/whitenoise/32/11_2.png) [@WhiteNoise](https://forum.obsidian.md/u/WhiteNoise)\
**Post date:** [March 24, 2026, 2:47pm UTC](https://forum.obsidian.md/t/quick-switcher-allow-searching-for-title-property-similar-to-aliases/112609/3 "2026-03-24T14:47:55Z")

</div>

I think you can post your idea as comment to this [FR](https://forum.obsidian.md/t/use-h1-or-yaml-property-title-instead-of-or-in-addition-to-filename-as-display-name/687), which should happen first.

---

<div class="post-metadata">

**Author:** ![Sunnaq445](https://forum.obsidian.md/letter_avatar_proxy/v4/letter/s/8c91f0/32.png) [@Sunnaq445](https://forum.obsidian.md/u/Sunnaq445)\
**Post date:** [March 24, 2026, 4:47pm UTC](https://forum.obsidian.md/t/quick-switcher-allow-searching-for-title-property-similar-to-aliases/112609/4 "2026-03-24T16:47:29Z")

</div>

You can bulk add title property key values to content with this Codescript Toolkit script:

```auto
import { App, Modal, Setting, TFolder, TFile, Notice } from "obsidian";

// Version 2

/**
 * =========================
 * CONFIG
 * =========================
 */
const CONFIG = {
    emptyLineBeforeHeading: false,
    emptyLineBeforeContent: false,
    skipIfAnyH1Exists: true,
    batchSize: 100,
    excludedFolders: [
        // Plain strings — exact match or direct parent
        ".trash",
        // Match folder name anywhere in a nested path (case-insensitive):
        /(^|\/)assets(\/|$)/i,
        /(^|\/)attachments(\/|$)/i,
        /(^|\/)templates(\/|$)/i,
    ] as Array<string | RegExp>
};

/**
 * Utility: Get all folders recursively
 */
function getAllFolders(app: App): TFolder[] {
    const folders: TFolder[] = [];
    const traverse = (folder: TFolder) => {
        folders.push(folder);
        folder.children.forEach(child => {
            if (child instanceof TFolder) traverse(child);
        });
    };
    traverse(app.vault.getRoot());
    return folders;
}

/**
 * Utility: Check if a folder path is excluded
 */
function isExcluded(folderPath: string): boolean {
    return CONFIG.excludedFolders.some(pattern => {
        if (pattern instanceof RegExp) {
            return pattern.test(folderPath);
        }
        // Plain string: exact match or direct child
        return folderPath === pattern || folderPath.startsWith(pattern + "/");
    });
}

/**
 * Utility: Get markdown files from selected folders (deduped, exclusions respected)
 */
function getFilesFromFolders(folders: TFolder[]): TFile[] {
    const seen = new Set<string>();
    const files: TFile[] = [];

    const traverse = (folder: TFolder) => {
        if (isExcluded(folder.path)) return;
        folder.children.forEach(child => {
            if (child instanceof TFolder) {
                traverse(child);
            } else if (child instanceof TFile && child.extension === "md") {
                if (!seen.has(child.path)) {
                    seen.add(child.path);
                    files.push(child);
                }
            }
        });
    };

    folders.forEach(traverse);
    return files;
}

/**
 * Extract title from YAML frontmatter
 */
function extractTitleFromYAML(content: string): { title: string | null, yamlEndIndex: number } {
    if (!content.startsWith("---")) return { title: null, yamlEndIndex: -1 };

    const end = content.indexOf("\n---", 3);
    if (end === -1) return { title: null, yamlEndIndex: -1 };

    const yamlBlock = content.substring(0, end + 4);
    const match = yamlBlock.match(/^title:\s*(.+)$/m);
    const title = match ? match[1].trim().replace(/^["']|["']$/g, "") : null;

    return { title, yamlEndIndex: end + 4 };
}

/**
 * Check if H1 exists immediately after YAML fence
 */
function hasH1AfterYaml(content: string, yamlEndIndex: number): boolean {
    const afterYaml = content.substring(yamlEndIndex).trimStart();
    return afterYaml.startsWith("# ");
}

/**
 * Check if ANY H1 exists anywhere in the file body (after YAML)
 */
function hasAnyH1InBody(content: string, yamlEndIndex: number): boolean {
    const body = content.substring(yamlEndIndex);
    return /^# .+/m.test(body);
}

/**
 * Check if a H1 with exactly the title text exists anywhere in the file body
 */
function hasMatchingH1InBody(content: string, yamlEndIndex: number, title: string): boolean {
    const body = content.substring(yamlEndIndex);
    const escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    return new RegExp(`^# ${escapedTitle}\\s*$`, "m").test(body);
}

/**
 * Insert H1 after YAML with CONFIG spacing
 */
function insertH1(content: string, yamlEndIndex: number, title: string): string {
    const before = content.substring(0, yamlEndIndex);
    const after = content.substring(yamlEndIndex);

    const beforeHeadingSpacing = CONFIG.emptyLineBeforeHeading ? "\n\n" : "\n";
    const afterHeadingSpacing = CONFIG.emptyLineBeforeContent ? "\n\n" : "\n";

    const afterTrimmed = after.replace(/^[\n\r]+/, "");

    return `${before}${beforeHeadingSpacing}# ${title}${afterHeadingSpacing}${afterTrimmed}`;
}

/**
 * Modal UI
 */
class FolderSelectModal extends Modal {
    selectedFolders: Set<string> = new Set();
    skipIfExists: boolean = true;
    onSubmit: (folders: TFolder[], skip: boolean) => void;

    constructor(app: App, onSubmit: (folders: TFolder[], skip: boolean) => void) {
        super(app);
        this.onSubmit = onSubmit;
    }

    onOpen() {
        const { contentEl } = this;
        contentEl.empty();
        contentEl.createEl("h2", { text: "Insert H1 from title property" });

        const allFolders = getAllFolders(this.app).filter(f => !isExcluded(f.path));
        const allSelected = allFolders.length > 0 && allFolders.every(f => this.selectedFolders.has(f.path));

        // --- Select All toggle ---
        new Setting(contentEl)
            .setName("Select all folders")
            .setHeading()
            .addToggle(toggle => {
                toggle.setValue(allSelected).onChange(value => {
                    if (value) allFolders.forEach(f => this.selectedFolders.add(f.path));
                    else this.selectedFolders.clear();
                    this.onOpen();
                });
            });

        contentEl.createEl("hr");

        // --- Individual folder toggles ---
        allFolders.forEach(folder => {
            new Setting(contentEl)
                .setName(folder.path || "/")
                .addToggle(toggle => {
                    toggle
                        .setValue(this.selectedFolders.has(folder.path))
                        .onChange(value => {
                            if (value) this.selectedFolders.add(folder.path);
                            else this.selectedFolders.delete(folder.path);
                        });
                });
        });

        contentEl.createEl("hr");

        // --- Skip if H1 already exists right after YAML ---
        new Setting(contentEl)
            .setName("Skip if H1 already exists after frontmatter")
            .addToggle(toggle => {
                toggle.setValue(this.skipIfExists).onChange(v => this.skipIfExists = v);
            });

        // --- Run button ---
        new Setting(contentEl)
            .addButton(btn =>
                btn.setButtonText("Run")
                    .setCta()
                    .onClick(() => {
                        const selected = allFolders.filter(f =>
                            this.selectedFolders.has(f.path)
                        );
                        if (selected.length === 0) {
                            new Notice("No folders selected.");
                            return;
                        }
                        this.close();
                        this.onSubmit(selected, this.skipIfExists);
                    })
            );
    }

    onClose() {
        this.contentEl.empty();
    }
}

/**
 * Main processing logic
 */
async function processFiles(app: App, folders: TFolder[], skipIfExists: boolean) {
    const files = getFilesFromFolders(folders);
    let updated = 0;
    let skipped = 0;
    let noTitle = 0;

    const progressNotice = new Notice(`Processing 0 / ${files.length}...`, 0);

    for (let i = 0; i < files.length; i++) {
        const file = files[i];

        // Yield to UI every batchSize files
        if (i > 0 && i % CONFIG.batchSize === 0) {
            progressNotice.setMessage(`Processing ${i} / ${files.length}...`);
            await sleep(10);
        }

        const content = await app.vault.read(file);
        const { title, yamlEndIndex } = extractTitleFromYAML(content);

        if (!title || yamlEndIndex === -1) {
            noTitle++;
            continue;
        }

        // Safety 1: skip if H1 immediately after YAML (modal toggle)
        if (skipIfExists && hasH1AfterYaml(content, yamlEndIndex)) {
            skipped++;
            continue;
        }

        // Safety 2: skip if ANY H1 exists anywhere in body (CONFIG flag)
        if (CONFIG.skipIfAnyH1Exists && hasAnyH1InBody(content, yamlEndIndex)) {
            skipped++;
            continue;
        }

        // Safety 3: skip if matching H1 exists anywhere in body (always-on)
        if (hasMatchingH1InBody(content, yamlEndIndex, title)) {
            skipped++;
            continue;
        }

        const newContent = insertH1(content, yamlEndIndex, title);
        if (newContent !== content) {
            await app.vault.modify(file, newContent);
            updated++;
        }
    }

    progressNotice.hide();
    new Notice(
        `Done — updated ${updated} file${updated !== 1 ? "s" : ""}, skipped ${skipped}, no title ${noTitle}`,
        8000
    );
}

/**
 * Entry point for CodeScript Toolkit
 */
export async function invoke(app: App): Promise<void> {
    new FolderSelectModal(app, (folders, skip) => {
        processFiles(app, folders, skip);
    }).open();
}

```

Save this script as `HeadingAdderBasedonTitleProp.ts` or whatever and place it in a folder you specify in the Codescript Toolkit plugin’s settings.

Then you can use the **[Quick Switcher++ plugin](https://github.com/darlal/obsidian-switcher-plus)** to get headings (any level) as results.

* * *

Script config:

```auto
    emptyLineBeforeHeading: false,
    emptyLineBeforeContent: false,

```

You can add true to either of these if you want empty lines after YAML fence and an empty line before your existing content.

```auto
    skipIfAnyH1Exists: true,

```

Means we skip processing any files with H1 in them, regardless if they equal title property value or not.

There is an interactive modal to pick all folders or individual folders but you can still add folders to excluded array in the syntax shown.

Test the script on not so important files or in a test vault.

This thread contains info how to set up the plugin to use these .ts files as commands:

> [@Plugin: CodeScript Toolkit](https://forum.obsidian.md/t/plugin-codescript-toolkit/94399):
>
> (formerly known as Fix Require Modules) This plugin is meant for the developers and other code writers. If you find the purpose of the plugin is not clear enough, I would be more than welcome to accept the PR to improve the README

In version 2, I added safety for handling thousands of files without potentially freezing Obsidian UI.

---

<div class="post-metadata">

**Author:** ![system](https://forum.obsidian.md/uploads/default/original/3X/b/e/bec35fbd23ab28909bb6bfe62cb32eb82dd34019.png) [@system](https://forum.obsidian.md/u/system)\
**Post date:** [June 22, 2026, 4:47pm UTC](https://forum.obsidian.md/t/quick-switcher-allow-searching-for-title-property-similar-to-aliases/112609/5 "2026-06-22T16:47:41Z")

</div>

Closed on June 22, 2026, 4:47pm UTC

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.
