add article
This commit is contained in:
parent
3d33c7ef84
commit
52c05d3ced
424
markdown/Autogen.space - Dynamically Generated Webpages.md
Normal file
424
markdown/Autogen.space - Dynamically Generated Webpages.md
Normal file
@ -0,0 +1,424 @@
|
||||
|
||||
|
||||
# Deep Dive: Building an AI-Powered Dynamic HTML Generator with Express, Groq SDK, and NodeCache
|
||||
|
||||
In this blog post, we’re going to dive into an advanced Node.js application that dynamically generates AI-powered HTML pages. We'll cover every aspect of the project—from setting up the server and caching mechanism to handling rate limits and errors when interfacing with an external AI service. This case study provides insight into high-end engineering decisions that help build a scalable, robust, and maintainable system.
|
||||
|
||||
## Introduction
|
||||
|
||||
Modern web applications demand real-time content generation and robust error handling. This project addresses several challenges:
|
||||
|
||||
- **Dynamic Content Generation:** Using AI to generate tailored HTML based on user requests.
|
||||
- **Efficient Caching:** Reducing repeated expensive API calls by caching generated HTML.
|
||||
- **Robust Error Handling:** Implementing exponential backoff to manage rate limits and transient server issues.
|
||||
- **Modular Design:** Separating concerns (cache management, AI communication, HTTP routing) for maintainability and scalability.
|
||||
- **Dynamic Subdomain Handling:** Utilizing wildcard DNS entries and a wildcard Virtual Host to route requests from any subdomain to the application, enabling multi-tenant support.
|
||||
|
||||
By analyzing each component of the code, you'll gain insights into how to build similar systems that rely on external APIs for dynamic content while ensuring high performance and reliability.
|
||||
|
||||
|
||||
|
||||
## Project Overview
|
||||
|
||||
The application listens for HTTP requests on an Express server and generates HTML pages dynamically. The high-level workflow is as follows:
|
||||
|
||||
1. **Keyword Extraction:**
|
||||
The system extracts a keyword from the request’s hostname. For multi-level domains, it uses the first subdomain (after replacing dashes with spaces) to determine the context; if no subdomain exists, it falls back to a default keyword.
|
||||
|
||||
2. **Cache Lookup:**
|
||||
The application first checks an in-memory cache (with disk persistence) to see if a page for the keyword has already been generated, thus avoiding unnecessary API calls.
|
||||
|
||||
3. **AI-Driven Content Generation:**
|
||||
- **Additional Context Request:** The system makes an initial request to the AI service to get contextual information about the keyword.
|
||||
- **HTML Generation Request:** Using the additional context, a second request generates a full HTML page styled with Bootstrap, enriched with SVG graphics, interactive modals, charts, and more.
|
||||
|
||||
4. **Caching and Response:**
|
||||
The generated HTML is stored in cache (and persisted to disk) and then served to the client.
|
||||
|
||||
5. **Error Handling and Rate Limiting:**
|
||||
The application implements an exponential backoff strategy for handling API rate limits and transient server errors, ensuring resilience and better user experience.
|
||||
|
||||
6. **Dynamic Subdomain Resolution:**
|
||||
By leveraging wildcard DNS entries and a wildcard Virtual Host, the application receives requests from any subdomain, which is then used to drive the dynamic HTML generation process.
|
||||
|
||||
|
||||
|
||||
## Detailed Code Walkthrough
|
||||
|
||||
Let's examine the code in detail, explaining the purpose and functionality of each segment.
|
||||
|
||||
### Dependency Imports and Environment Setup
|
||||
|
||||
The application begins with importing the necessary modules and configuring environment variables.
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import Groq from 'groq-sdk';
|
||||
import 'dotenv/config';
|
||||
import NodeCache from 'node-cache';
|
||||
import fs from 'fs';
|
||||
```
|
||||
|
||||
- **Express:** A minimal and flexible Node.js web application framework for building APIs.
|
||||
- **Groq SDK:** Provides a client to interact with the AI API.
|
||||
- **dotenv:** Loads environment variables from a `.env` file for secure configuration.
|
||||
- **NodeCache:** A simple in-memory caching solution with TTL (time-to-live) support.
|
||||
- **fs:** The Node.js file system module, used for cache persistence.
|
||||
|
||||
We then define constants and create our primary objects:
|
||||
|
||||
```javascript
|
||||
const CACHE_FILE = 'cache.json';
|
||||
const app = express();
|
||||
const port = 8998;
|
||||
const cache = new NodeCache({ stdTTL: 30 * 24 * 60 * 60, checkperiod: 60 * 60 }); // 30 days TTL, checks every hour
|
||||
```
|
||||
|
||||
**Highlights:**
|
||||
|
||||
- **Cache Lifetime:** The cache is configured with a 30-day TTL and an hourly check to remove expired entries.
|
||||
- **Port Assignment:** The Express server listens on port `8998`.
|
||||
|
||||
|
||||
|
||||
### Cache Management and Persistence
|
||||
|
||||
Caching is essential to reduce repeated expensive API calls. Here, NodeCache is used for in-memory caching, and the cache is serialized to a file for persistence across server restarts.
|
||||
|
||||
#### Loading Cache from Disk
|
||||
|
||||
When the server starts, it attempts to load a previously saved cache:
|
||||
|
||||
```javascript
|
||||
function loadCacheFromDisk() {
|
||||
if (fs.existsSync(CACHE_FILE)) {
|
||||
try {
|
||||
const data = fs.readFileSync(CACHE_FILE, 'utf8');
|
||||
const jsonData = JSON.parse(data);
|
||||
for (const [key, value] of Object.entries(jsonData)) {
|
||||
cache.set(key, value);
|
||||
}
|
||||
console.log('✅ Cache loaded from disk');
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading cache from disk:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Considerations:**
|
||||
|
||||
- **Robustness:** The code checks if the cache file exists and handles any errors during file reading or JSON parsing.
|
||||
- **Granularity:** Each key-value pair is loaded into the in-memory cache, ensuring that subsequent requests can be served quickly.
|
||||
|
||||
#### Saving Cache to Disk
|
||||
|
||||
After updates to the cache, the new state is saved back to disk:
|
||||
|
||||
```javascript
|
||||
function saveCacheToDisk() {
|
||||
try {
|
||||
const data = JSON.stringify(cache.mget(cache.keys()), null, 2);
|
||||
fs.writeFileSync(CACHE_FILE, data, 'utf8');
|
||||
console.log('💾 Cache saved to disk');
|
||||
} catch (error) {
|
||||
console.error('❌ Error saving cache to disk:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Highlights:**
|
||||
|
||||
- **Serialization:** The entire cache is serialized in a human-readable JSON format.
|
||||
- **Synchronization:** This persistence mechanism helps to ensure that the cache survives server restarts, reducing the need to re-fetch data from the AI API.
|
||||
|
||||
|
||||
|
||||
### Initializing the Groq Client and AI Request Handling
|
||||
|
||||
The Groq client is configured using the API key stored in the environment, with settings for retries and timeouts:
|
||||
|
||||
```javascript
|
||||
const client = new Groq({
|
||||
apiKey: process.env['GROQ_API_KEY'],
|
||||
maxRetries: 2,
|
||||
timeout: 20 * 1000, // 20 seconds timeout per request
|
||||
});
|
||||
```
|
||||
|
||||
**Considerations:**
|
||||
|
||||
- **Security:** The API key is securely loaded from environment variables.
|
||||
- **Timeouts and Retries:** Configuring retries and timeouts ensures that API calls do not hang indefinitely, which is crucial for maintaining a responsive server.
|
||||
|
||||
#### Utility Function: `sleep`
|
||||
|
||||
To handle delays between retry attempts, a simple sleep function is implemented:
|
||||
|
||||
```javascript
|
||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
```
|
||||
|
||||
This function is used to introduce delays, implementing an exponential backoff mechanism when rate limits are encountered.
|
||||
|
||||
#### AI Request with Rate Limit Handling
|
||||
|
||||
The function `requestWithRateLimitHandling` abstracts the logic of making requests to the AI API while managing errors and rate limits.
|
||||
|
||||
```javascript
|
||||
async function requestWithRateLimitHandling(prompt) {
|
||||
let attempt = 0;
|
||||
const maxAttempts = 5;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
try {
|
||||
const response = await client.chat.completions.create({
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
model: 'llama-3.1-8b-instant',
|
||||
});
|
||||
console.log('✅ Response received');
|
||||
return response.choices[0].message.content;
|
||||
} catch (err) {
|
||||
if (err instanceof Groq.APIError) {
|
||||
console.error(`❌ Error ${err.status}: ${err.name}`);
|
||||
|
||||
if (err.status === 429) { // Rate limit handling
|
||||
const retryAfterMs = (2 ** attempt) * 1000;
|
||||
console.log(`⚠️ Rate limit hit. Retrying in ${retryAfterMs / 1000} seconds...`);
|
||||
await sleep(retryAfterMs);
|
||||
attempt++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (err.status >= 500 || err.status === 408 || err.status === 409) { // Server errors
|
||||
const retryAfterMs = (2 ** attempt) * 1000;
|
||||
console.log(`⚠️ Server error. Retrying in ${retryAfterMs / 1000} seconds...`);
|
||||
await sleep(retryAfterMs);
|
||||
attempt++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
throw new Error('❌ Max retry attempts reached, request failed.');
|
||||
}
|
||||
```
|
||||
|
||||
**Important Aspects:**
|
||||
|
||||
- **Exponential Backoff:** Each retry increases the delay exponentially (`2^attempt * 1000 ms`), which helps mitigate further rate limit issues.
|
||||
- **Error Differentiation:** The code distinguishes between rate limit errors (HTTP 429) and server errors (HTTP 5xx, 408, 409), allowing for tailored retry strategies.
|
||||
- **Fail-Safe:** After exhausting the maximum attempts, the function throws an error, enabling the calling code to handle the failure appropriately.
|
||||
|
||||
|
||||
|
||||
### Express Route: Dynamic HTML Generation
|
||||
|
||||
The core of the application is the Express route that generates HTML content based on the incoming request. This route encapsulates the entire workflow from keyword extraction to AI-driven HTML generation and caching.
|
||||
|
||||
```javascript
|
||||
app.get('*', async (req, res) => {
|
||||
try {
|
||||
const hostHeader = req.headers.host || '';
|
||||
const hostname = hostHeader.split(':')[0];
|
||||
const parts = hostname.split('.');
|
||||
let keyword = parts.length > 2 ? parts[0].replace(/-/g, ' ') : 'default';
|
||||
|
||||
// Check cache first
|
||||
const cachedHTML = cache.get(keyword);
|
||||
if (cachedHTML) {
|
||||
console.log(`⚡ Serving from cache: ${keyword}`);
|
||||
res.set('Content-Type', 'text/html');
|
||||
return res.send(cachedHTML);
|
||||
}
|
||||
|
||||
console.log(`🆕 Fetching additional context for: ${keyword}`);
|
||||
|
||||
// First AI request: Get additional context about the keyword
|
||||
const contextPrompt = `
|
||||
Act as a research assistant tasked with providing a comprehensive overview of the topic "${keyword}". In your explanation, describe what makes this subject fascinating by highlighting its significance, sharing any notable historical or amusing anecdotes, and outlining its key components. Ensure your response is engaging, clear, and easily digestible, keeping in mind that it will be used by another AI.
|
||||
`;
|
||||
const additionalContext = await requestWithRateLimitHandling(contextPrompt);
|
||||
|
||||
console.log(`🔍 Additional context retrieved: ${additionalContext.slice(0, 100)}...`);
|
||||
|
||||
console.log(`🆕 Generating new content for: ${keyword}`);
|
||||
|
||||
// Second AI request: Generate HTML using the additional context
|
||||
const prompt = `You are an AI web designer in 2025. Generate a fully structured, valid HTML5 webpage based on the keyword: "${keyword}". Use the following additional context for "${keyword}":
|
||||
${additionalContext}
|
||||
|
||||
Requirements:
|
||||
• Start with a proper <!DOCTYPE html> declaration and include well-formed <html>, <head>, and <body> sections.
|
||||
• Use Bootstrap for styling and layout—incorporate all of its components—and integrate FontAwesome icons.
|
||||
• Place all CSS inside <style></style> tags in the head; do not use external stylesheets.
|
||||
• Text content must be vast and informative.
|
||||
• Theme the page and its functionality to reflect the keyword. For example, if the keyword is "stopwatch," create a stopwatch app; if the keyword is "List App," build a list app. If a specific function is not clear, design a page that reflects ideas and concepts related to the keyword.
|
||||
• Use only SVG graphics (do not use images in other formats).
|
||||
• Design a rich, engaging layout with plenty of relevant, humorous, and meme-inspired content (emojis are welcome).
|
||||
• Include multiple interactive modals with unique content. All navigation should open modals (do not use anchor links or traditional navigation menus).
|
||||
• If possible, incorporate charts using Chart.js and display interesting tables.
|
||||
• Add cool effects and custom scrollbars where appropriate.
|
||||
• Do not add any "Learn More" or "Read More" buttons unless they open a modal.
|
||||
• Do not include any contact forms or any extra explanatory text.
|
||||
• The webpage URL is: https://${keyword}.autogen.space/
|
||||
• Output only the complete HTML code (with embedded CSS) for the webpage. Do not include any extra text, explanations, notes, or commentary.
|
||||
• Do not include the word “html” or any other text before the HTML code.
|
||||
• Do not include any note or code block at the end—only output the HTML code.`;
|
||||
|
||||
const generatedHTML = await requestWithRateLimitHandling(prompt);
|
||||
let replacedHTML = generatedHTML.replace(/[\s\S]*?/g, '');
|
||||
|
||||
// Store in cache
|
||||
cache.set(keyword, replacedHTML);
|
||||
saveCacheToDisk(); // Save to disk after updating cache
|
||||
|
||||
res.set('Content-Type', 'text/html');
|
||||
res.send(replacedHTML);
|
||||
} catch (error) {
|
||||
console.error('🚨 Error generating HTML:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Workflow Breakdown:**
|
||||
|
||||
1. **Keyword Extraction:**
|
||||
- The hostname is parsed and segmented. For domains with multiple subdomains, the first subdomain (with dashes replaced by spaces) is used as the keyword.
|
||||
- If the domain does not match the expected format, a default keyword is applied.
|
||||
|
||||
2. **Cache Check:**
|
||||
- The cache is queried to see if an HTML page for the given keyword already exists. If found, the cached HTML is immediately served, improving response time.
|
||||
|
||||
3. **AI-Driven Context and HTML Generation:**
|
||||
- The first AI call generates additional context for the keyword, providing a rich background that informs the final HTML generation.
|
||||
- The second AI call uses this context to generate a fully structured HTML page. The generated code adheres strictly to specified constraints, ensuring a modern, engaging, and interactive webpage.
|
||||
|
||||
4. **Caching and Persistence:**
|
||||
- Once generated, the HTML is cached in memory and persisted to disk, ensuring that subsequent requests can bypass the expensive AI calls.
|
||||
- This step is critical for performance optimization, especially under heavy load.
|
||||
|
||||
5. **Error Handling:**
|
||||
- Any errors encountered during the process trigger a fallback mechanism, logging the error and returning a 500 status code.
|
||||
|
||||
|
||||
|
||||
### DNS and Virtual Host Configuration
|
||||
|
||||
A key enabler of the dynamic subdomain functionality in this system is the proper configuration of DNS and Virtual Hosts.
|
||||
|
||||
#### Wildcard DNS Entries
|
||||
|
||||
Since the application extracts keywords from the subdomain portion of the URL (e.g., `keyword.example.com`), a wildcard DNS entry is essential. Configuring an A record like:
|
||||
|
||||
```
|
||||
*.example.com IN A <your-server-IP>
|
||||
```
|
||||
|
||||
ensures that any subdomain request, regardless of the value before the domain (e.g., `foo.example.com`, `bar.example.com`), resolves to your server. This flexibility is critical for dynamically handling multiple tenant-like requests without needing separate DNS entries for each.
|
||||
|
||||
#### Wildcard Virtual Host
|
||||
|
||||
Equally important is the configuration of a wildcard Virtual Host on your web server (such as Nginx or Apache). For instance, an Nginx configuration block might look like this:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name *.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8998;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This configuration directs all subdomain requests to the Node.js application, ensuring that the extracted subdomain (i.e., the keyword) is available for processing. Without a wildcard Virtual Host, your server might only handle requests for a single domain, breaking the dynamic HTML generation workflow.
|
||||
|
||||
By integrating wildcard DNS entries and a wildcard Virtual Host, the system robustly supports multi-tenancy and ensures that every subdomain request reaches the application for dynamic processing.
|
||||
|
||||
|
||||
|
||||
## Error Handling, Rate Limiting, and Retry Strategy
|
||||
|
||||
Robust error handling is fundamental in distributed systems, especially when dealing with external APIs. In our application:
|
||||
|
||||
- **Categorization of Errors:**
|
||||
Errors are categorized based on their HTTP status codes:
|
||||
- **429 (Too Many Requests):** Indicates rate limiting, triggering an exponential backoff.
|
||||
- **408, 409, 5xx (Server Errors):** Also retried with increasing delays.
|
||||
|
||||
- **Exponential Backoff:**
|
||||
With each retry, the delay doubles, mitigating the risk of overwhelming the API further while increasing the chance that transient errors will resolve.
|
||||
|
||||
- **Retry Limit:**
|
||||
The application gives up after five attempts, ensuring that it doesn’t get stuck in an infinite loop. This allows the Express route to return a proper error response to the client.
|
||||
|
||||
|
||||
|
||||
## Advanced Considerations
|
||||
|
||||
Beyond the basic functionality, several advanced topics are worth exploring for a production-grade implementation.
|
||||
|
||||
### Scalability and Concurrency
|
||||
|
||||
- **Load Balancing:**
|
||||
In high-traffic scenarios, consider deploying multiple instances of the application behind a load balancer. This architecture ensures that requests are distributed evenly, reducing response times and improving reliability.
|
||||
|
||||
- **Cluster Mode:**
|
||||
Use Node.js's cluster module to spawn multiple worker processes on multi-core systems. This improves CPU utilization and can handle higher concurrency.
|
||||
|
||||
- **Distributed Caching:**
|
||||
For a horizontally scaled environment, in-memory caches like NodeCache might need to be replaced or supplemented with a distributed caching solution like Redis to maintain consistency across multiple instances.
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- **API Key Management:**
|
||||
Ensure API keys and sensitive configurations are stored securely. Use environment variables or secrets management tools to avoid exposure.
|
||||
|
||||
- **Input Sanitization:**
|
||||
Although the AI prompts are largely pre-defined, always validate and sanitize any input derived from HTTP headers to prevent injection attacks.
|
||||
|
||||
- **HTTPS Enforcement:**
|
||||
Running the application behind a reverse proxy (like Nginx) with SSL/TLS ensures that all communications are secure.
|
||||
|
||||
### Monitoring and Observability
|
||||
|
||||
- **Logging:**
|
||||
Enhance the logging mechanism to include structured logs (JSON format) for better integration with log aggregators and monitoring tools.
|
||||
|
||||
- **Metrics and Alerts:**
|
||||
Integrate monitoring tools like Prometheus and Grafana to track API response times, error rates, and cache hit/miss ratios. Alerts can be set up to notify the operations team in case of abnormal behavior.
|
||||
|
||||
- **Tracing:**
|
||||
Implement distributed tracing (e.g., using OpenTelemetry) to gain insights into the end-to-end request flow, particularly useful when diagnosing performance issues or API bottlenecks.
|
||||
|
||||
## My thoghts
|
||||
|
||||
In this post we dissected a modern, AI-powered dynamic HTML generator built with Node.js. The application showcases several advanced engineering techniques:
|
||||
|
||||
- **Express Routing:**
|
||||
A flexible and powerful HTTP server that orchestrates the flow from incoming requests to dynamic content generation.
|
||||
|
||||
- **AI Integration via Groq SDK:**
|
||||
Leveraging AI to provide contextual understanding and to generate rich HTML content dynamically.
|
||||
|
||||
- **Efficient Caching Strategies:**
|
||||
Combining in-memory caching with disk persistence to minimize redundant API calls and reduce latency.
|
||||
|
||||
- **Robust Error Handling:**
|
||||
Implementing exponential backoff and retry strategies to gracefully manage API rate limits and transient errors.
|
||||
|
||||
- **Dynamic Subdomain Resolution:**
|
||||
The use of wildcard DNS entries and a wildcard Virtual Host is essential to direct all subdomain traffic to the application, enabling it to extract keywords dynamically and generate tailored content.
|
||||
|
||||
- **Advanced Engineering Considerations:**
|
||||
Addressing scalability, security, and observability to build a production-ready system.
|
||||
|
||||
This architecture is an excellent example of how modern web applications can harness AI to deliver highly personalized and interactive content in real time. Whether you’re building an AI assistant, a dynamic content generator, or an interactive web application, the patterns demonstrated here will guide you in designing resilient, scalable, and maintainable systems.
|
||||
|
||||
Happy coding!
|
||||
|
Loading…
x
Reference in New Issue
Block a user