Framework: Zola
Hosting: Github Pages
Theme: Consoler Dark
Title: parkerjones.dev

Zola Surreal DB site cache

2025-01-10
Published by Parker Jones
6 minute read

Overview

  1. Understanding SurrealDB and Browser Compatibility
  2. Generating a SurrealDB SQL File During Site Build
  3. Serving the SQL File from Your Site
  4. Loading SurrealDB in the Browser
  5. Implementing Full-Text Search and Taxonomy Support
  6. Putting It All Together

1. Understanding SurrealDB and Browser Compatibility

What is SurrealDB?

SurrealDB is a scalable, distributed database designed to be flexible and efficient. It's capable of handling traditional relational data, document data, and graph data, and it provides an SQL-like query language.

Browser Compatibility as of January 2025

As of January 2025, SurrealDB has made significant strides in terms of client-side capabilities. With the advancements in WebAssembly (WASM) and web technologies, it's now possible to run SurrealDB in-memory in the browser using WASM.

Key Points:


2. Generating a SurrealDB SQL File During Site Build

To populate the in-memory database in the browser, we'll generate a SurrealDB SQL file containing all the site content and metadata.

2.1. Extracting Site Content and Metadata

We'll use Zola's data directory and global variables to access all pages, sections, taxonomies, and their metadata.

2.2. Creating a Template to Generate the SQL File

We'll create a template that iterates over all the pages and constructs SQL CREATE and INSERT statements.

Create a Template File

{% raw %}
-- SurrealDB SQL Export
-- Generated on {{ now() | date(format="%Y-%m-%d %H:%M:%S") }}

-- Drop existing tables (if any)
REMOVE TABLE page;
REMOVE TABLE section;
REMOVE TABLE taxonomy_term;
REMOVE TABLE taxonomy_item;

-- Create tables
DEFINE TABLE page SCHEMAFULL
    PERMISSIONS FOR ALL NONE;

DEFINE TABLE section SCHEMAFULL
    PERMISSIONS FOR ALL NONE;

DEFINE TABLE taxonomy_term SCHEMAFULL
    PERMISSIONS FOR ALL NONE;

DEFINE TABLE taxonomy_item SCHEMAFULL
    PERMISSIONS FOR ALL NONE;

-- Insert pages
{% for page in pages %}
CREATE page:{{ page.slug }} CONTENT {
    title: {{ page.title | json_encode() }},
    slug: {{ page.slug | json_encode() }},
    path: {{ page.permalink | json_encode() }},
    content: {{ page.content | json_encode() }},
    summary: {{ page.summary | json_encode() }},
    date: {{ page.date | date(format="%Y-%m-%dT%H:%M:%SZ") | json_encode() }},
    taxonomies: [
        {% for key, terms in page.taxonomies %}
            { name: {{ key | json_encode() }}, terms: [ {{ terms | map(attribute="name") | join(", ") | json_encode() }} ] },
        {% endfor %}
    ],
    metadata: {{ page.extra | json_encode() }}
};

{% endfor %}

-- Insert sections
{% for section in sections %}
CREATE section:{{ section.slug }} CONTENT {
    title: {{ section.title | json_encode() }},
    slug: {{ section.slug | json_encode() }},
    path: {{ section.permalink | json_encode() }},
    content: {{ section.content | json_encode() }},
    summary: {{ section.summary | json_encode() }},
    date: {{ section.date | date(format="%Y-%m-%dT%H:%M:%SZ") | json_encode() }},
    metadata: {{ section.extra | json_encode() }}
};

{% endfor %}

-- Insert taxonomy terms
{% for kind, terms in taxonomies %}
    {% for term in terms %}
CREATE taxonomy_term:{{ term.name | slugify }} CONTENT {
    kind: {{ kind | json_encode() }},
    name: {{ term.name | json_encode() }},
    slug: {{ term.slug | json_encode() }},
    path: {{ term.permalink | json_encode() }},
    pages: [ {% for p in term.pages %} page:{{ p.slug }}, {% endfor %} ]
};
    {% endfor %}
{% endfor %}

-- Relationships between pages and taxonomies can be defined here if needed

{% endraw %}

Explanation:

2.3. Configuring the Template for Output

We need to ensure Zola processes our template and outputs the generated SQL file.

Adjust config.toml

Add an entry to extra to indicate where to output the SQL file.

[extra]
surrealdb_sql_output = "static/surrealdb_export.sql"

Alternatively, you can hardcode the output path in your build process.

2.4. Create a Build Script to Generate the SQL File

We'll use a build script to render the template.

Create generate_surrealdb_sql.sh

#!/bin/bash

# Ensure the script exits if any command fails
set -e

# Render the SQL template
zola build -o /tmp/zola_build_sql --template-only
# Copy the rendered file to the desired location
cp /tmp/zola_build_sql/surrealdb_export.sql static/
# Clean up temporary build directory
rm -rf /tmp/zola_build_sql

echo "SurrealDB SQL export generated successfully."

Explanation:

Make the Script Executable

chmod +x generate_surrealdb_sql.sh

2.5. Update Your Build Process

Ensure that the generate_surrealdb_sql.sh script runs before the site is built.

Locally

./generate_surrealdb_sql.sh && zola build

On Netlify

Update your netlify.toml or build command to include the script.

[build]
  publish = "public"
  command = "./generate_surrealdb_sql.sh && zola build"

3. Serving the SQL File from Your Site

By placing surrealdb_export.sql in the static/ directory, Zola will serve it at the root of your site.

Ensure that it's accessible and not blocked by any server rules.


4. Loading SurrealDB in the Browser

4.1. Using the SurrealDB WebAssembly Build

As of January 2025, SurrealDB provides a WebAssembly (WASM) build that can be run in the browser.

Including SurrealDB WASM in Your Site

4.2. Initializing SurrealDB in the Browser

<script>
  // Load the SurrealDB WASM module
  Surreal.initWasm({
    path: '/js/surrealdb.wasm'
  }).then(async () => {
    // Create a new in-memory SurrealDB instance
    const db = new Surreal();

    // Sign in as a root user (authentication can be bypassed in-memory)
    await db.signin({ user: 'root', pass: 'root' });

    // Select a namespace and database
    await db.use({ ns: 'myapp', db: 'mydb' });

    // Fetch the SQL file
    const response = await fetch('/surrealdb_export.sql');
    const sql = await response.text();

    // Execute the SQL statements to populate the database
    await db.query(sql);

    // Now the database is ready for use
    // You can perform queries, full-text search, etc.

    // Example query
    const pages = await db.select('page');

    console.log('Pages:', pages);

    // Implement search functionality and other features as needed

  }).catch((e) => {
    console.error('Error initializing SurrealDB:', e);
  });
</script>

Explanation:


5. Implementing Full-Text Search and Taxonomy Support

With the database populated, you can perform complex queries, including full-text search and navigating taxonomies.

SurrealDB supports full-text search indexes.

Define Full-Text Search Indexes in the SQL

Modify your SQL template to include index definitions.

-- Define full-text search index on page content
DEFINE INDEX page_content_ft ON TABLE page FIELDS content SEARCH ANALYZER bm25 TOKENIZERS blank;

-- Similarly, you can define indexes on other fields as needed

Note: Ensure that your version of SurrealDB supports the specific index definitions. Syntax may vary; refer to the latest documentation.

Performing a Full-Text Search in JavaScript

// Function to perform a full-text search
async function searchPages(query) {
  // Use the 'search' function in your SurrealQL query
  const results = await db.query(`
    SELECT * FROM page WHERE search(content, $terms)
  `, { terms: query });

  return results[0].result;
}

// Example usage
const searchResults = await searchPages('your search terms');
console.log('Search Results:', searchResults);

5.2. Taxonomy Structure and Metadata

You can query pages by taxonomy terms.

Querying Pages by Taxonomy Term

async function getPagesByTaxonomy(termName) {
  const results = await db.query(`
    SELECT * FROM page WHERE $term INSIDE taxonomies[*].terms
  `, { term: termName });

  return results[0].result;
}

// Example usage
const pages = await getPagesByTaxonomy('tech');
console.log('Pages in Tech taxonomy:', pages);

Explanation:

5.3. Accessing Metadata Defined in Front Matter

The metadata field in each page or section contains the extra data from the front matter.

async function getPageMetadata(slug) {
  const page = await db.select(`page:${slug}`);
  return page.metadata;
}

// Example usage
const metadata = await getPageMetadata('my-page-slug');
console.log('Page Metadata:', metadata);

6. Putting It All Together

6.1. Search Interface

Create a search input field in your HTML.

<input type="text" id="search-input" placeholder="Search...">
<div id="search-results"></div>
document.getElementById('search-input').addEventListener('input', async (event) => {
  const query = event.target.value;
  
  if (query.length > 2) { // Start searching after 3 characters
    const results = await searchPages(query);
    displaySearchResults(results);
  } else {
    clearSearchResults();
  }
});

function displaySearchResults(results) {
  const resultsDiv = document.getElementById('search-results');
  resultsDiv.innerHTML = '';

  results.forEach((page) => {
    const pageLink = document.createElement('a');
    pageLink.href = page.path;
    pageLink.textContent = page.title;
    resultsDiv.appendChild(pageLink);
  });
}

function clearSearchResults() {
  const resultsDiv = document.getElementById('search-results');
  resultsDiv.innerHTML = '';
}

6.3. Handling Taxonomy Navigation

// Example function to list all taxonomy terms
async function listTaxonomyTerms(kind) {
  const results = await db.query(`
    SELECT * FROM taxonomy_term WHERE kind = $kind
  `, { kind });

  return results[0].result;
}

// Display taxonomy terms
const terms = await listTaxonomyTerms('tags');
terms.forEach((term) => {
  console.log('Term:', term.name);
});

Considerations and Best Practices

Data Size and Performance

Security

Browser Compatibility

Licensing and Permissions


Alternative Solutions

If using SurrealDB client-side becomes challenging due to data size, performance, or compatibility, consider these alternatives:

Lunr.js

Elasticlunr.js

Algolia


Conclusion

By generating a SurrealDB SQL file during your site's build process and leveraging the in-browser capabilities of SurrealDB with WebAssembly, you can create a powerful and dynamic search experience on your static site.


Next Steps


If you need further assistance with any of these steps or have additional questions, feel free to ask! I'm here to help you create the best possible experience for your site.