ai-search-indexer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/examples/index-sites.js +22 -0
- package/examples/index-wikipedia.js +25 -0
- package/package.json +29 -0
- package/src/index.js +1 -0
- package/src/indexer.js +185 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @website-grabber/indexer
|
|
2
|
+
|
|
3
|
+
Website content indexer using Mozilla Readability and Playwright. Extracts only relevant content from web pages, filtering out navigation, ads, and UI elements.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 Smart content extraction with Mozilla Readability
|
|
8
|
+
- 🎯 Full SPA support (Angular, React, Vue, etc.)
|
|
9
|
+
- 📄 Extracts articles, documentation, and code examples
|
|
10
|
+
- âš¡ Powered by Playwright for accurate rendering
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @website-grabber/indexer
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import { WebsiteIndexer } from '@website-grabber/indexer';
|
|
22
|
+
|
|
23
|
+
const indexer = new WebsiteIndexer('./context.json');
|
|
24
|
+
|
|
25
|
+
await indexer.indexUrls([
|
|
26
|
+
'https://angular.dev/overview',
|
|
27
|
+
'https://reactjs.org/docs',
|
|
28
|
+
'https://vuejs.org/guide'
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
indexer.displayStats();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
### WebsiteIndexer
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
const indexer = new WebsiteIndexer(contextFile);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Methods:**
|
|
43
|
+
|
|
44
|
+
- `indexUrls(urls: string[]): Promise<void>` - Index multiple URLs
|
|
45
|
+
- `indexUrl(page, url: string): Promise<object>` - Index a single URL
|
|
46
|
+
- `saveContext(): void` - Save indexed content to file
|
|
47
|
+
- `loadContext(): object` - Load indexed content from file
|
|
48
|
+
- `displayStats(): void` - Display indexing statistics
|
|
49
|
+
|
|
50
|
+
## Output Format
|
|
51
|
+
|
|
52
|
+
Indexed content is saved as JSON:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"indexed_content": [
|
|
57
|
+
{
|
|
58
|
+
"url": "https://example.com",
|
|
59
|
+
"title": "Page Title",
|
|
60
|
+
"description": "Meta description",
|
|
61
|
+
"content": "Extracted main content...",
|
|
62
|
+
"html_content": "<div>HTML with code examples</div>",
|
|
63
|
+
"excerpt": "Brief excerpt...",
|
|
64
|
+
"word_count": 2500,
|
|
65
|
+
"indexed_at": "2026-02-12T18:00:00.000Z",
|
|
66
|
+
"site_name": "Example Site"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"metadata": {
|
|
70
|
+
"last_updated": "2026-02-12T18:00:00.000Z",
|
|
71
|
+
"total_pages": 1,
|
|
72
|
+
"version": "1.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- Node.js 18+
|
|
80
|
+
- Playwright (automatically installs browser)
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { WebsiteIndexer } from '../src/index.js';
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const urls = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
if (urls.length === 0) {
|
|
8
|
+
console.log('Usage: node examples/index-sites.js <url1> <url2> ...');
|
|
9
|
+
console.log('Example: node examples/index-sites.js https://example.com');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log(`Starting indexer for ${urls.length} URL(s)...\n`);
|
|
14
|
+
|
|
15
|
+
const indexer = new WebsiteIndexer('./context.json');
|
|
16
|
+
await indexer.indexUrls(urls);
|
|
17
|
+
indexer.displayStats();
|
|
18
|
+
|
|
19
|
+
console.log('Indexing complete!');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { WebsiteIndexer } from '../src/index.js';
|
|
3
|
+
import { resolve, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const wikipediaUrl = 'https://de.wikipedia.org/wiki/Eisenbahnunfall_im_Gotthard-Basistunnel';
|
|
10
|
+
|
|
11
|
+
console.log(`Starting indexer for Wikipedia article...\n`);
|
|
12
|
+
console.log(`URL: ${wikipediaUrl}\n`);
|
|
13
|
+
|
|
14
|
+
// Save context to a location accessible by the web components demo
|
|
15
|
+
const contextPath = resolve(__dirname, '../../web-components/demo/context.json');
|
|
16
|
+
|
|
17
|
+
const indexer = new WebsiteIndexer(contextPath);
|
|
18
|
+
await indexer.indexUrls([wikipediaUrl]);
|
|
19
|
+
indexer.displayStats();
|
|
20
|
+
|
|
21
|
+
console.log(`\nIndexing complete!`);
|
|
22
|
+
console.log(`Context saved to: ${contextPath}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-search-indexer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Website content indexer using Mozilla Readability and Playwright",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"indexer",
|
|
12
|
+
"readability",
|
|
13
|
+
"playwright",
|
|
14
|
+
"content-extraction",
|
|
15
|
+
"web-scraping"
|
|
16
|
+
],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@mozilla/readability": "^0.5.0",
|
|
21
|
+
"playwright": "^1.58.2"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"example": "node examples/index-sites.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebsiteIndexer } from './indexer.js';
|
package/src/indexer.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Indexes web pages and extracts their content using Mozilla Readability
|
|
6
|
+
* Supports Angular, React, Vue and other SPAs
|
|
7
|
+
*/
|
|
8
|
+
export class WebsiteIndexer {
|
|
9
|
+
constructor(contextFile = './context.json') {
|
|
10
|
+
this.contextFile = contextFile;
|
|
11
|
+
this.context = this.loadContext();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
loadContext() {
|
|
15
|
+
try {
|
|
16
|
+
const data = readFileSync(this.contextFile, 'utf8');
|
|
17
|
+
return JSON.parse(data);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error loading context file:', error);
|
|
20
|
+
return {
|
|
21
|
+
indexed_content: [],
|
|
22
|
+
metadata: {
|
|
23
|
+
last_updated: null,
|
|
24
|
+
total_pages: 0,
|
|
25
|
+
version: '1.0'
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
saveContext() {
|
|
32
|
+
this.context.metadata.last_updated = new Date().toISOString();
|
|
33
|
+
this.context.metadata.total_pages = this.context.indexed_content.length;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(
|
|
37
|
+
this.contextFile,
|
|
38
|
+
JSON.stringify(this.context, null, 2),
|
|
39
|
+
'utf8'
|
|
40
|
+
);
|
|
41
|
+
console.log(`Context saved successfully. Total pages: ${this.context.metadata.total_pages}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error saving context file:', error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract main content using Mozilla Readability via Playwright
|
|
49
|
+
* Extracts only relevant long-form content and code examples
|
|
50
|
+
* Works with Angular, React, Vue and other SPAs by using rendered DOM
|
|
51
|
+
*/
|
|
52
|
+
async extractMainContent(page) {
|
|
53
|
+
try {
|
|
54
|
+
// Add Readability script to the page and extract content
|
|
55
|
+
await page.addScriptTag({
|
|
56
|
+
path: './node_modules/@mozilla/readability/Readability.js'
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Extract content using Readability in the browser context
|
|
60
|
+
const article = await page.evaluate(() => {
|
|
61
|
+
const documentClone = document.cloneNode(true);
|
|
62
|
+
const reader = new window.Readability(documentClone);
|
|
63
|
+
return reader.parse();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!article) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
title: article.title,
|
|
72
|
+
textContent: article.textContent,
|
|
73
|
+
htmlContent: article.content,
|
|
74
|
+
excerpt: article.excerpt,
|
|
75
|
+
length: article.length,
|
|
76
|
+
siteName: article.siteName
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Error extracting content with Readability:', error.message);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Index a single URL
|
|
86
|
+
*/
|
|
87
|
+
async indexUrl(page, url) {
|
|
88
|
+
try {
|
|
89
|
+
console.log(`Indexing: ${url}`);
|
|
90
|
+
|
|
91
|
+
await page.goto(url, {
|
|
92
|
+
waitUntil: 'networkidle',
|
|
93
|
+
timeout: 30000
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Wait for Angular/React/Vue apps to fully render
|
|
97
|
+
// Wait for network to be idle and a bit more for client-side rendering
|
|
98
|
+
await page.waitForTimeout(3000);
|
|
99
|
+
|
|
100
|
+
// Extract main content using Readability on the rendered page
|
|
101
|
+
const article = await this.extractMainContent(page);
|
|
102
|
+
|
|
103
|
+
if (!article) {
|
|
104
|
+
console.log(`No main content found for: ${url}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get metadata
|
|
109
|
+
const metadata = await page.evaluate(() => {
|
|
110
|
+
return {
|
|
111
|
+
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Create indexed entry with only relevant content
|
|
116
|
+
const indexedEntry = {
|
|
117
|
+
url: url,
|
|
118
|
+
title: article.title,
|
|
119
|
+
description: metadata.description || article.excerpt,
|
|
120
|
+
content: article.textContent.substring(0, 15000), // Store clean text content
|
|
121
|
+
html_content: article.htmlContent, // Store HTML for code examples
|
|
122
|
+
excerpt: article.excerpt,
|
|
123
|
+
indexed_at: new Date().toISOString(),
|
|
124
|
+
word_count: article.length,
|
|
125
|
+
site_name: article.siteName
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Check if URL already exists and update or add
|
|
129
|
+
const existingIndex = this.context.indexed_content.findIndex(item => item.url === url);
|
|
130
|
+
if (existingIndex >= 0) {
|
|
131
|
+
this.context.indexed_content[existingIndex] = indexedEntry;
|
|
132
|
+
console.log(`✓ Updated: ${url} (${article.length} words)`);
|
|
133
|
+
} else {
|
|
134
|
+
this.context.indexed_content.push(indexedEntry);
|
|
135
|
+
console.log(`✓ Added: ${url} (${article.length} words)`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return indexedEntry;
|
|
139
|
+
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`✗ Error indexing ${url}:`, error.message);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Index multiple URLs
|
|
148
|
+
*/
|
|
149
|
+
async indexUrls(urls) {
|
|
150
|
+
const browser = await chromium.launch({
|
|
151
|
+
headless: true
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const page = await browser.newPage();
|
|
156
|
+
|
|
157
|
+
for (const url of urls) {
|
|
158
|
+
await this.indexUrl(page, url);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.saveContext();
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Error during indexing:', error);
|
|
165
|
+
} finally {
|
|
166
|
+
await browser.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Display statistics
|
|
172
|
+
*/
|
|
173
|
+
displayStats() {
|
|
174
|
+
console.log('\n=== Indexing Statistics ===');
|
|
175
|
+
console.log(`Total pages indexed: ${this.context.metadata.total_pages}`);
|
|
176
|
+
console.log(`Last updated: ${this.context.metadata.last_updated}`);
|
|
177
|
+
|
|
178
|
+
if (this.context.indexed_content.length > 0) {
|
|
179
|
+
const totalWords = this.context.indexed_content.reduce((sum, item) => sum + item.word_count, 0);
|
|
180
|
+
console.log(`Total words indexed: ${totalWords}`);
|
|
181
|
+
console.log(`Average words per page: ${Math.round(totalWords / this.context.indexed_content.length)}`);
|
|
182
|
+
}
|
|
183
|
+
console.log('==========================\n');
|
|
184
|
+
}
|
|
185
|
+
}
|