8000 Adding Site-Wide Search to Ignite by JPToroDev · Pull Request #819 · twostraws/Ignite · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Adding Site-Wide Search to Ignite #819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Adding Site-Wide Search to Ignite #819

wants to merge 1 commit into from

Conversation

JPToroDev
Copy link
Collaborator

This PR offers a possible implementation of site-wide search leveraging lunr.js, which integrates with Ignite with a single JS file, much like Prism. During publishing, we use the title, description, and url properties of all pages—and also the tags and date properties of articles—to build a search index that lunr queries via its JS.

Pages that lack a description will be ignored and emit a warning during publishing.

The API looks like this:

SearchForm { result in
    result.title
        .font(.title4)
        .fontWeight(.bold)
        .foregroundStyle(.bootstrapPurple)
    result.description
        .foregroundStyle(.black)
        .lineSpacing(1.2)
} noResultsView: {
    Text("No results were found!")
} resultsPageHeader: {
    Text("Looking for something?")
}

And looks like this in action:

Image

You can see site-wide search in action on this branch of IgniteSamples.

@twostraws
Copy link
Owner

Conceptually the idea of site-wide search is great, and would be a welcome addition. I have my usual concerns about adding a third-party dependency, particularly for filtering based on only three properties, but putting that to one side this feels like an odd API shape. If this were a regular Ignite page type (alongside StaticPage and ArticlePage) then users could design it however they wanted, and access a searchResults property via protocol extension that provides an array of each article's title, description, URL, image, tags, etc.

@JPToroDev
Copy link
Collaborator Author

particularly for filtering based on only three properties

The implementation uses only three properties for standard pages—and five properties for articles—to prevent performance issues. A search index is a JSON file hosted on the website. Say a website has 100 searchable pages: indexing three properties that naturally have the highest concentration of keywords creates a much smaller index file than indexing the entire body of each page—which would create an index file that is essentially 100 pages in size.

This "tradeoff" isn't unique to lunr.js, but to client-side search in general. That said, I think the solution—encouraging folks to write highly searchable titles and descriptions—is a good thing.

this feels like an odd API shape. If this were a regular Ignite page type

I had considered making a SearchPage type initially, but that API design is at odds with how the HTML needs to be generated.

The view used for a search result needs to be defined by itself, because it creates a reusable <template> element.

Something like this isn't possible:

struct MySearchPage: SearchPage {
    var body: some HTML {
        Text("Search Results")
        ForEach(searchResults) {
            result.title
        }
    }
}

Because we need to generate HTML that looks like this for the search result:

<template>
    <p class="result-title"></p>
</template>

But we don't know where the reusable template starts and ends in the view tree of MySearchPage.

To make a SearchPage type work, we'd need an API like this:

struct MySearchPage: SearchPage {

    func resultView(result: SearchResult)  some HTML {
         result.title
            .font(.title4)
            .fontWeight(.bold)
            .foregroundStyle(.bootstrapPurple)
        result.description
            .foregroundStyle(.black)
            .lineSpacing(1.2)
    }

    var body: some HTML {
        ForEach(searchResults) { // implicit property, but this property could also be used in the above method, which shouldn't have access to it
            result.title
        }
    }
}

Or this:

struct MySearchPage: SearchPage {

    func resultView(result: SearchResult) -> some HTML {
         result.title
            .font(.title4)
            .fontWeight(.bold)
            .foregroundStyle(.bootstrapPurple)
        result.description
            .foregroundStyle(.black)
            .lineSpacing(1.2)
    }

    func body(searchResults: [SearchResultView]) -> some HTML {
        ForEach(searchResults) {
            result.title
        }
    }

    func noResultsView() -> some HTML {
        Text("No results...")
    }
}

Those API shapes look like nothing else in Ignite.

What's more, the current API allows people to create different search pages for each instance of SearchForm. That wouldn't be possible if users were to add a single SearchPage conforming type to a searchPage: any SearchPage requirement of Site.

@MrSkwiggs
Copy link
Collaborator

Something like this isn't possible:

struct MySearchPage: SearchPage {
    var body: some HTML {
        Text("Search Results")
        ForEach(searchResults) {
            result.title
        }
    }
}

Because we need to generate HTML that looks like this for the search result:

<template>
<p class="result-title"></p>
</template>

But we don't know where the reusable template starts and ends in the view tree of MySearchPage.

Could we perhaps specialise ForEach when contained in a SearchPage-conforming type? Or override its declaration in SearchPage-conforming types? And wrap its content with <template> anchors.

This would be somewhat transparent to the user while giving us all the necessary control required for this sort of behaviour 🤔

@twostraws
Copy link
Owner

Why is that <template> code even needed? I'd like to think users could present their search results however they wanted, for example using a List or similar.

@MrSkwiggs
Copy link
Collaborator

Why is that <template> code even needed? I'd like to think users could present their search results however they wanted, for example using a List or similar.

I believe the reason is simply because the results & html elements are generated at runtime and need to be populated dynamically? We can define the <template> at compile-time for it to be reused by JS. I don't know that we could achieve this with raw a Ignite declaration?

@JPToroDev
Copy link
Collaborator Author

I believe the reason is simply because the results & html elements are generated at runtime and need to be populated dynamically?

@MrSkwiggs is correct. The <template> tag essentially says: Ignore this element for the time being; at some point in the future, it'll be populated dynamically by JS.

@JPToroDev
Copy link
Collaborator Author

Could we perhaps specialise ForEach when contained in a SearchPage-conforming type? Or override its declaration in SearchPage-conforming types? And wrap its content with template anchors.

Unfortunately not. We can specialize a type based only on its children, not its parent.

@JPToroDev
Copy link
Collaborator Author

If we wanted to use a ForEach, perhaps something like this would be our best bet?

struct MySearchPage: SearchPage {
    var body: some HTML {
        Text("Search Results")
        ForEach(searchResults) {
            SearchResult {
                result.title
            }
        }
    }
}

@MrSkwiggs
Copy link
Collaborator
MrSkwiggs commented Jun 18, 2025

Could we perhaps specialise ForEach when contained in a SearchPage-conforming type? Or override its declaration in SearchPage-conforming types? And wrap its content with template anchors.

Unfortunately not. We can specialize a type based only on its children, not its parent.

What about the "override its declaration in SearchPage-conforming types"?

We could do something like:

public extension SearchPage {
  struct ForEach {
    // Use regular ForEach under the hood but wrap contents with the extra <template> anchors
  }
}

@JPToroDev
Copy link
Collaborator Author

Unfortunately you can't nest types inside protocols in Swift. But that idea has put us on the right path!

What we could do is create a custom initializer for ForEach that takes our SearchResults type. Then the body of that initializer would wrap its content in a <template>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0