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 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
+ }