The Search Feature

The Search page embeds the Search App Scripts app.  The search feature displays a search box and search button that stays static at the top of the page.  When the user enters any search text it escaped into text characters only so that potential hacking tricks cannot get performed.   A favorite trick of hackers is to use the input boxes to send executable code to the server.  If it is text characters only then it will not get executed as code.

When the user hits the search button the API that handles searching will be called.  This API call will accept the search text, find any matches and then return a Json object that contains the search results.  The search results display under the top search bar.  The Json object are fields the search results display will fill in and display to the user.

The frontend code will accept the Json and loop through the object to display it into the Search Card Interface.  This is done for up to 50 records.  We are only getting the first 50 records because most will find what they seek within that first 50 records or try a different search.  If we pull 100+ records, it will be a waste of server resources in almost all searches.

If the user refreshes a search the page will reload the current Json object's data.  A new call to the API will not occur, that only occur when the search button is clicked.

App Scripts code engine is used.  The code between the "<?  ?>" represents executable App Scripts engine code.  This code will only execute on the App Scripts servers.  This is why we embed the app into the page instead of copying the code into the page.


In the <head> tag is where we'd place local JavaScript for the Search feature.  This is frontend code.  We do some validation and call the Code file to get the search results.  We place custom CSS code that the loaded CSS library, W3.CSS, doesn't handle.


 The frontend code is below.   Note the versions of libraries.  This affects what can be done.  This is a temp layout and API.


Code View

Code.gs
// Code.gs
/**
 * Serves the initial HTML page (Index.html).
 * @returns {GoogleAppsScript.HTML.HtmlOutput} The HTML output for the web app.
 */
function doGet() {
  // Create a template from the Index.html file.
  const template = HtmlService.createTemplateFromFile('Index');
  // Evaluate the template and set the title.
  return template.evaluate()
      .setTitle('W3.CSS Search App');
}

/**
 * Includes an HTML file as a template.
 * This function is used within HTML templates (e.g., Index.html) to include other HTML files.
 * @param {string} filename The name of the HTML file to include (without .html extension).
 * @returns {string} The content of the included HTML file.
 */
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

/**
 * Performs a search using the Open Library API and returns structured results.
 * Implements basic server-side validation and error handling.
 * @param {string} searchText The text to search for.
 * @returns {Object} An object containing search results or an error message.
 */
function performSearch(searchText) {
  // Basic server-side validation to prevent obvious XSS and empty queries.
  if (!searchText || typeof searchText !== 'string' || searchText.trim() === '') {
    return { error: 'Search text cannot be empty.' };
  }

  // Sanitize input: remove potential script tags or event handlers.
  // Although HtmlService.HtmlOutput.evaluate() handles output escaping,
  // it's good practice to validate input before using it in a URL fetch.
  const sanitizedSearchText = searchText.replace(/<script.*?>.*?<\/script>/gi, '')
                                        .replace(/on\w+=".*?"/gi, '')
                                        .trim();

  if (sanitizedSearchText === '') {
    return { error: 'Invalid search text after sanitization.' };
  }

  // Encode the search text for URL.
  const encodedSearchText = encodeURIComponent(sanitizedSearchText);
  const apiUrl = `https://openlibrary.org/search.json?q=${encodedSearchText}&limit=50`; // Limit to 50 results as requested.

  try {
    // Fetch data from the Open Library API.
    const response = UrlFetchApp.fetch(apiUrl);
    const jsonResponse = JSON.parse(response.getContentText());

    // Process the results.
    const results = [];
    if (jsonResponse && jsonResponse.docs) {
      jsonResponse.docs.forEach(doc => {
        results.push({
          title: doc.title || 'No Title',
          author: doc.author_name ? doc.author_name.join(', ') : 'Unknown Author',
          firstPublishYear: doc.first_publish_year || 'N/A',
          // Construct a cover image URL if available
          coverUrl: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-M.jpg` : 'https://placehold.co/128x192/cccccc/333333?text=No+Cover'
        });
      });
    }
    return { success: true, results: results };

  } catch (e) {
    // Log the error for debugging.
    console.error('Error fetching from Open Library API:', e.message);
    return { error: 'Failed to fetch search results. Please try again later.' };
  }
}
 

Index.html

<!-- Index.html -->
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <title>W3.CSS Search App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://www.w3schools.com/w3css/5/w3.css">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
  <style>
    body, h1, h2, h3, h4, h5, h6 {font-family: "Inter", sans-serif;}
    .w3-card { border-radius: 8px; }
    .w3-button { border-radius: 8px; }
    .w3-input { border-radius: 8px; }
    .w3-container { padding: 16px; }
    .w3-center { text-align: center; }
    .w3-padding-large { padding: 24px !important; }
    .w3-margin-top { margin-top: 16px !important; }
    .w3-margin-bottom { margin-bottom: 16px !important; }
    .w3-display-middle {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      -ms-transform: translate(-50%, -50%);
    }
    .loading-spinner {
      border: 4px solid #f3f3f3; /* Light grey */
      border-top: 4px solid #3498db; /* Blue */
      border-radius: 50%;
      width: 30px;
      height: 30px;
      animation: spin 1s linear infinite;
      margin: 20px auto;
      display: none; /* Hidden by default */
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    /* Responsive grid for results */
    .w3-row-padding .w3-col {
      padding: 8px;
    }
    @media (min-width: 601px) {
      .w3-half { width: 50%; }
      .w3-third { width: 33.33%; }
      .w3-quarter { width: 25%; }
    }
  </style>
</head>
<body class="w3-light-grey">

  <!-- Header -->
  <div class="w3-bar w3-blue-grey w3-card-4 w3-round-large w3-padding-small">
    <a href="#" class="w3-bar-item w3-button w3-round-large w3-hover-light-grey" onclick="loadHomePage()">Home</a>
    <span class="w3-bar-item w3-right w3-large">Public Search App</span>
  </div>

  <!-- Main content area where pages will be loaded -->
  <div id="content-area" class="w3-container w3-padding-large w3-margin-top">
    <?!= include('Home'); ?>
  </div>

  <!-- Loading Spinner -->
  <div id="loadingSpinner" class="loading-spinner"></div>

  <script>
    // Client-side JavaScript for handling page transitions and search.

    /**
     * Shows or hides the loading spinner.
     * @param {boolean} show True to show, false to hide.
     */
    function toggleLoadingSpinner(show) {
      document.getElementById('loadingSpinner').style.display = show ? 'block' : 'none';
    }

    /**
     * Loads the Home page content into the main content area.
     */
    function loadHomePage() {
      toggleLoadingSpinner(true);
      google.script.run
        .withSuccessHandler(function(html) {
          document.getElementById('content-area').innerHTML = html;
          toggleLoadingSpinner(false);
        })
        .withFailureHandler(function(error) {
          loadErrorPage(error.message); // Load error page on failure
          toggleLoadingSpinner(false);
        })
        .include('Home'); // Call the server-side include function
    }

    /**
     * Performs a search when the search button is clicked.
     */
    function performSearch() {
      const searchInput = document.getElementById('searchInput');
      let searchText = searchInput.value;

      // Client-side XSS validation: Basic check for script tags and event handlers.
      // This is a first line of defense; server-side validation is more critical.
      const scriptRegex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
      const eventHandlerRegex = /on\w+="[^"]*"/gi;

      if (scriptRegex.test(searchText) || eventHandlerRegex.test(searchText)) {
        loadErrorPage('Invalid characters detected. Please remove script tags or event handlers.');
        return;
      }

      if (searchText.trim() === '') {
        loadErrorPage('Please enter some text to search.');
        return;
      }

      toggleLoadingSpinner(true); // Show spinner

      // Call the server-side function to perform the search.
      google.script.run
        .withSuccessHandler(function(response) {
          toggleLoadingSpinner(false); // Hide spinner
          if (response.success) {
            // Load the Results.html template with the fetched data.
            loadResultsPage(response.results);
          } else {
            loadErrorPage(response.error || 'An unknown error occurred during search.');
          }
        })
        .withFailureHandler(function(error) {
          toggleLoadingSpinner(false); // Hide spinner
          loadErrorPage('Error during search: ' + error.message);
        })
        .performSearch(searchText);
    }

    /**
     * Loads the Results page content into the main content area with search results.
     * @param {Array} results An array of search result objects.
     */
    function loadResultsPage(results) {
      toggleLoadingSpinner(true);
      // Call the server-side include function for Results.html.
      // Pass the results data to the template.
      google.script.run
        .withSuccessHandler(function(html) {
          // Replace placeholders in the HTML with actual data.
          let populatedHtml = html;
          if (results && results.length > 0) {
            let resultsHtml = '';
            results.forEach(item => {
              resultsHtml += `
                <div class="w3-col l3 m4 s6 w3-margin-bottom">
                  <div class="w3-card w3-white w3-round-large w3-hover-shadow">
                    <img src="${item.coverUrl}" alt="Book Cover" class="w3-image w3-round-t-large" style="width:100%; height: 192px; object-fit: cover;" onerror="this.onerror=null;this.src='[https://placehold.co/128x192/cccccc/333333?text=No+Cover](https://placehold.co/128x192/cccccc/333333?text=No+Cover)';">
                    <div class="w3-container w3-padding-small">
                      <h4 class="w3-text-blue-grey w3-small"><b>${item.title}</b></h4>
                      <p class="w3-tiny">Author: ${item.author}</p>
                      <p class="w3-tiny">Published: ${item.firstPublishYear}</p>
                    </div>
                  </div>
                </div>
              `;
            });
            populatedHtml = populatedHtml.replace('<!-- SEARCH_RESULTS_PLACEHOLDER -->', resultsHtml);
          } else {
            populatedHtml = populatedHtml.replace('<!-- SEARCH_RESULTS_PLACEHOLDER -->', '<p class="w3-center w3-text-grey">No results found for your search.</p>');
          }
          document.getElementById('content-area').innerHTML = populatedHtml;
          toggleLoadingSpinner(false);
        })
        .withFailureHandler(function(error) {
          loadErrorPage('Failed to load results page: ' + error.message); // Load error page on failure
          toggleLoadingSpinner(false);
        })
        .include('Results'); // Call the server-side include function
    }

    /**
     * Loads the custom error page with a given message.
     * @param {string} errorMessage The message to display on the error page.
     */
    function loadErrorPage(errorMessage) {
      toggleLoadingSpinner(true);
      google.script.run
        .withSuccessHandler(function(html) {
          // Replace the placeholder in the error HTML with the actual message.
          const populatedHtml = html.replace('<!-- ERROR_MESSAGE_PLACEHOLDER -->', errorMessage);
          document.getElementById('content-area').innerHTML = populatedHtml;
          toggleLoadingSpinner(false);
        })
        .withFailureHandler(function(error) {
          // Fallback if even the error page fails to load
          document.getElementById('content-area').innerHTML = `
            <div class="w3-panel w3-red w3-round-large w3-padding-large w3-margin-top w3-center">
              <h3>Critical Error!</h3>
              <p>Could not load error page. Original error: ${errorMessage}</p>
              <p>Further error: ${error.message}</p>
              <button class="w3-button w3-white w3-round-large w3-margin-top" onclick="loadHomePage()">Go to Home</button>
            </div>
          `;
          toggleLoadingSpinner(false);
        })
        .include('Error'); // Call the server-side include function for Error.html
    }
  </script>
</body>
</html>


Home.html
<!-- Home.html -->
<div class="w3-container w3-card-4 w3-white w3-round-large w3-padding-large w3-margin-top w3-center">
  <h2 class="w3-text-blue-grey">Welcome to the Public Search App!</h2>
  <p>Enter your search query below to find books from Open Library.</p>

  <div class="w3-row w3-section w3-center">
    <div class="w3-col" style="width:50px"><i class="w3-xxlarge fa fa-search"></i></div>
    <div class="w3-rest">
      <input class="w3-input w3-border w3-round-large" name="search" type="text" placeholder="Search for books..." id="searchInput" onkeydown="if(event.keyCode === 13) performSearch()">
    </div>
  </div>

  <button class="w3-button w3-blue-grey w3-hover-light-grey w3-round-large w3-padding-large" onclick="performSearch()">
    <i class="fa fa-search"></i> Search
  </button>

  <div class="w3-panel w3-light-grey w3-round-large w3-padding-large w3-margin-top w3-border w3-border-blue-grey">
    <h3 class="w3-text-blue-grey">Ad Space</h3>
    <p>Your advertisement could go here! This space is available for promotions.</p>
    <img src="https://placehold.co/300x100/4CAF50/ffffff?text=Your+Ad+Here" alt="Advertisement" class="w3-image w3-round-large" style="max-width:100%;">
    <p class="w3-small w3-text-grey">Contact us for ad placement.</p>
  </div>
</div>
 


Results.html

<!-- Results.html -->
<div class="w3-container w3-card-4 w3-white w3-round-large w3-padding-large w3-margin-top">
  <h2 class="w3-text-blue-grey w3-center">Search Results</h2>
  <p class="w3-center w3-text-grey">Displaying up to 50 results from Open Library.</p>

  <div class="w3-row-padding w3-margin-top">
    <!-- SEARCH_RESULTS_PLACEHOLDER -->
    <!-- This placeholder will be replaced by JavaScript with the actual search results -->
  </div>

  <div class="w3-center w3-margin-top">
    <button class="w3-button w3-blue-grey w3-hover-light-grey w3-round-large" onclick="loadHomePage()">Back to Home</button>
  </div>
</div>





Error.html

<!-- Results.html -->
<div class="w3-container w3-card-4 w3-white w3-round-large w3-padding-large w3-margin-top">
  <h2 class="w3-text-blue-grey w3-center">Search Results</h2>
  <p class="w3-center w3-text-grey">Displaying up to 50 results from Open Library.</p>

  <div class="w3-row-padding w3-margin-top">
    <!-- SEARCH_RESULTS_PLACEHOLDER -->
    <!-- This placeholder will be replaced by JavaScript with the actual search results -->
  </div>

  <div class="w3-center w3-margin-top">
    <button class="w3-button w3-blue-grey w3-hover-light-grey w3-round-large" onclick="loadHomePage()">Back to Home</button>
  </div>
</div>



The Gemini text to get the code with only a few moderations I had to make as noted:


Create a multiple page search app using w3.css and google app script. Use app scripts multi page html templating features.  Load a Home html template page as default, allow ad space on it using W3.CSS. The Index page should have a search bar for users to enter the search text and click a search button to load the Results html template page using W3.CSS. Validate the text input to avoid cross script hacking.

Create code in the Code.gs that uses the text, validates it and send it to the search API to et search results.  When the user hits the search button load the Results html template page interface into the index page. Use a public API to test display results up to 50 records from the public API in a grid display. The interface should be a card using w3.css. The Home page is the default load. The Results page load into the Index when the search button is clicked.

Important Considerations:

Public API Key Security: If your chosen public API requires an API key, do not expose it directly in client-side JavaScript. Instead, store it securely in your Code.gs file and make all API calls from the Google Apps Script backend (using UrlFetchApp). This way, your API key is never visible to the end-user.

Error Handling: Implement more robust error handling on both the client and server sides.

Pagination/Load More: For APIs returning more than 50 results, you'll need to implement pagination (e.g., "next page" button) or infinite scrolling. This would involve passing offset or page parameters to your performSearch function.

Rate Limiting: Be mindful of rate limits imposed by the public API you choose.

Real XSS Prevention: The provided client-side XSS validation is basic. For production applications, consider using a dedicated JavaScript sanitization library (like DOMPurify if you're working with DOM manipulation directly) or rely heavily on server-side output encoding (which Apps Script often handles well when using HtmlService).

User Experience: Add loading spinners, empty state messages, and clearer error messages.

Search Filters/Sorting: Extend the search interface to include options for filtering or sorting results.

Favicon: Add a favicon to your HTML.

Responsive Design: W3.CSS is inherently responsive, but always test on different screen sizes.

CSS Customization: Further customize the W3.CSS styles to match your brand.

Can you add error checking and custom Error html template page that loads on error with error message. Use W3.CSS to style it.