adstxt-validator 1.2.3

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,806 @@
1
+ # @miyaichi/ads-txt-validator
2
+
3
+ A comprehensive TypeScript library for parsing, validating, and cross-checking ads.txt files against sellers.json data. This package provides robust validation capabilities with detailed error reporting and optimization features.
4
+
5
+ > **Note**: This package is available on GitHub Packages. For internal use within the adstxt-manager monorepo, the package is also available as `@adstxt-manager/ads-txt-validator`.
6
+
7
+ ## Features
8
+
9
+ - **Complete ads.txt parsing**: Parse ads.txt files and extract records and variables
10
+ - **Sellers.json cross-checking**: Validate ads.txt entries against sellers.json specifications
11
+ - **Duplicate detection**: Identify duplicate entries across ads.txt files
12
+ - **Content optimization**: Remove duplicates and standardize format
13
+ - **Comprehensive validation**: Multiple validation levels with detailed error reporting
14
+ - **Internationalized messages**: Multi-language support with configurable help URLs
15
+ - **External URL configuration**: Configure base URLs for help links when used as external library
16
+ - **TypeScript support**: Full TypeScript support with detailed type definitions
17
+
18
+ ## Installation
19
+
20
+ ### From GitHub Packages
21
+
22
+ ```bash
23
+ npm install @miyaichi/ads-txt-validator
24
+ ```
25
+
26
+ ### From internal monorepo
27
+
28
+ ```bash
29
+ npm install @adstxt-manager/ads-txt-validator
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### Basic Usage
35
+
36
+ ```typescript
37
+ import { parseAdsTxtContent, crossCheckAdsTxtRecords } from '@miyaichi/ads-txt-validator';
38
+
39
+ // Parse ads.txt content
40
+ const adsTxtContent = `
41
+ example.com, pub-1234, DIRECT
42
+ reseller.com, reseller-5678, RESELLER
43
+ CONTACT=admin@example.com
44
+ `;
45
+
46
+ const parsedEntries = parseAdsTxtContent(adsTxtContent, 'example.com');
47
+
48
+ // Legacy approach (still supported)
49
+ const getSellersJson = async (domain: string) => {
50
+ const response = await fetch(`https://${domain}/sellers.json`);
51
+ return response.json();
52
+ };
53
+
54
+ const validatedEntries = await crossCheckAdsTxtRecords(
55
+ 'publisher.com',
56
+ parsedEntries,
57
+ null, // cached ads.txt content
58
+ getSellersJson
59
+ );
60
+ ```
61
+
62
+ ### Optimized Usage (Recommended)
63
+
64
+ For better performance, especially with large sellers.json files, use the new `SellersJsonProvider` interface:
65
+
66
+ ```typescript
67
+ import {
68
+ parseAdsTxtContent,
69
+ crossCheckAdsTxtRecords,
70
+ SellersJsonProvider,
71
+ } from '@miyaichi/ads-txt-validator';
72
+
73
+ // Create optimized provider
74
+ const sellersJsonProvider: SellersJsonProvider = {
75
+ async batchGetSellers(domain: string, sellerIds: string[]) {
76
+ // Efficiently fetch only needed sellers
77
+ const result = await fetchSellersFromDatabase(domain, sellerIds);
78
+ return {
79
+ domain,
80
+ requested_count: sellerIds.length,
81
+ found_count: result.foundCount,
82
+ results: result.sellers,
83
+ metadata: result.metadata,
84
+ cache: result.cacheInfo,
85
+ };
86
+ },
87
+
88
+ async hasSellerJson(domain: string) {
89
+ return await checkSellerJsonExists(domain);
90
+ },
91
+
92
+ async getMetadata(domain: string) {
93
+ return await getSellerJsonMetadata(domain);
94
+ },
95
+
96
+ async getCacheInfo(domain: string) {
97
+ return await getCacheInformation(domain);
98
+ },
99
+ };
100
+
101
+ // Use optimized validation
102
+ const validatedEntries = await crossCheckAdsTxtRecords(
103
+ 'publisher.com',
104
+ parsedEntries,
105
+ null,
106
+ sellersJsonProvider
107
+ );
108
+ ```
109
+
110
+ ## API Reference
111
+
112
+ ### Core Functions
113
+
114
+ #### `parseAdsTxtContent(content: string, publisherDomain?: string): ParsedAdsTxtEntry[]`
115
+
116
+ Parses complete ads.txt file content and returns an array of parsed entries.
117
+
118
+ **Parameters:**
119
+
120
+ - `content`: Raw ads.txt file content
121
+ - `publisherDomain`: Optional publisher domain for default OWNERDOMAIN
122
+
123
+ **Returns:** Array of `ParsedAdsTxtEntry` objects
124
+
125
+ #### `parseAdsTxtLine(line: string, lineNumber: number): ParsedAdsTxtEntry | null`
126
+
127
+ Parses a single line from an ads.txt file.
128
+
129
+ **Parameters:**
130
+
131
+ - `line`: Single line from ads.txt file
132
+ - `lineNumber`: Line number for error reporting
133
+
134
+ **Returns:** `ParsedAdsTxtEntry` object or `null` for comments/empty lines
135
+
136
+ #### `crossCheckAdsTxtRecords(publisherDomain: string, parsedEntries: ParsedAdsTxtEntry[], cachedAdsTxtContent: string | null, sellersJsonProvider: SellersJsonProvider): Promise<ParsedAdsTxtEntry[]>`
137
+
138
+ **Optimized cross-check function (recommended)** - Cross-checks parsed entries against existing ads.txt and sellers.json data using efficient selective queries.
139
+
140
+ **Parameters:**
141
+
142
+ - `publisherDomain`: Publisher's domain for validation
143
+ - `parsedEntries`: Array of parsed ads.txt entries
144
+ - `cachedAdsTxtContent`: Existing ads.txt content for duplicate detection
145
+ - `sellersJsonProvider`: Optimized provider for sellers.json data
146
+
147
+ **Returns:** Promise resolving to enhanced entries with validation results
148
+
149
+ #### `crossCheckAdsTxtRecords(publisherDomain: string, parsedEntries: ParsedAdsTxtEntry[], cachedAdsTxtContent: string | null, getSellersJson: (domain: string) => Promise<any>): Promise<ParsedAdsTxtEntry[]>`
150
+
151
+ **Legacy cross-check function** - Cross-checks parsed entries against existing ads.txt and sellers.json data.
152
+
153
+ **Parameters:**
154
+
155
+ - `publisherDomain`: Publisher's domain for validation
156
+ - `parsedEntries`: Array of parsed ads.txt entries
157
+ - `cachedAdsTxtContent`: Existing ads.txt content for duplicate detection
158
+ - `getSellersJson`: Function to fetch complete sellers.json data
159
+
160
+ **Returns:** Promise resolving to enhanced entries with validation results
161
+
162
+ **Note:** This overload is deprecated in favor of the `SellersJsonProvider` version for better performance.
163
+
164
+ #### `optimizeAdsTxt(content: string, publisherDomain?: string): string`
165
+
166
+ Optimizes ads.txt content by removing duplicates and standardizing format.
167
+
168
+ **Parameters:**
169
+
170
+ - `content`: Raw ads.txt content
171
+ - `publisherDomain`: Optional publisher domain
172
+
173
+ **Returns:** Optimized ads.txt content string
174
+
175
+ #### `isValidEmail(email: string): boolean`
176
+
177
+ Validates email addresses with comprehensive regex.
178
+
179
+ **Parameters:**
180
+
181
+ - `email`: Email address to validate
182
+
183
+ **Returns:** Boolean indicating validity
184
+
185
+ ### Message System Functions
186
+
187
+ #### `configureMessages(config: MessageConfig): void`
188
+
189
+ Configures the global message provider with base URL and locale settings.
190
+
191
+ **Parameters:**
192
+
193
+ - `config`: Configuration object with optional `defaultLocale` and `baseUrl`
194
+
195
+ #### `createValidationMessage(key: string, placeholders?: string[], locale?: string): ValidationMessage | null`
196
+
197
+ Creates a localized validation message with formatted help URLs.
198
+
199
+ **Parameters:**
200
+
201
+ - `key`: Validation error key (e.g., 'domainMismatch')
202
+ - `placeholders`: Array of values to substitute in message templates
203
+ - `locale`: Target locale (defaults to configured locale)
204
+
205
+ **Returns:** `ValidationMessage` object or null if key not found
206
+
207
+ #### `setMessageProvider(provider: MessageProvider): void`
208
+
209
+ Sets a custom message provider for advanced usage.
210
+
211
+ **Parameters:**
212
+
213
+ - `provider`: Custom message provider implementation
214
+
215
+ #### `getMessageProvider(): MessageProvider`
216
+
217
+ Gets the current message provider.
218
+
219
+ **Returns:** Current `MessageProvider` instance
220
+
221
+ ### Type Definitions
222
+
223
+ #### `SellersJsonProvider`
224
+
225
+ Interface for optimized sellers.json data access:
226
+
227
+ ```typescript
228
+ interface SellersJsonProvider {
229
+ batchGetSellers(domain: string, sellerIds: string[]): Promise<BatchSellersResult>;
230
+ getMetadata(domain: string): Promise<SellersJsonMetadata>;
231
+ hasSellerJson(domain: string): Promise<boolean>;
232
+ getCacheInfo(domain: string): Promise<CacheInfo>;
233
+ }
234
+ ```
235
+
236
+ #### `BatchSellersResult`
237
+
238
+ Result structure for batch seller queries:
239
+
240
+ ```typescript
241
+ interface BatchSellersResult {
242
+ domain: string;
243
+ requested_count: number;
244
+ found_count: number;
245
+ results: SellerResult[];
246
+ metadata: SellersJsonMetadata;
247
+ cache: CacheInfo;
248
+ }
249
+ ```
250
+
251
+ #### `SellerResult`
252
+
253
+ Individual seller query result:
254
+
255
+ ```typescript
256
+ interface SellerResult {
257
+ sellerId: string;
258
+ seller: Seller | null;
259
+ found: boolean;
260
+ source: 'cache' | 'fresh';
261
+ error?: string;
262
+ }
263
+ ```
264
+
265
+ #### `Seller`
266
+
267
+ Seller information from sellers.json:
268
+
269
+ ```typescript
270
+ interface Seller {
271
+ seller_id: string;
272
+ name?: string;
273
+ domain?: string;
274
+ seller_type?: 'PUBLISHER' | 'INTERMEDIARY' | 'BOTH';
275
+ is_confidential?: 0 | 1;
276
+ [key: string]: any;
277
+ }
278
+ ```
279
+
280
+ #### `SellersJsonMetadata`
281
+
282
+ Metadata from sellers.json file:
283
+
284
+ ```typescript
285
+ interface SellersJsonMetadata {
286
+ version?: string;
287
+ contact_email?: string;
288
+ contact_address?: string;
289
+ seller_count?: number;
290
+ identifiers?: any[];
291
+ }
292
+ ```
293
+
294
+ #### `CacheInfo`
295
+
296
+ Cache information:
297
+
298
+ ```typescript
299
+ interface CacheInfo {
300
+ is_cached: boolean;
301
+ last_updated?: string;
302
+ status: 'success' | 'error' | 'stale';
303
+ expires_at?: string;
304
+ }
305
+ ```
306
+
307
+ #### `ParsedAdsTxtEntry`
308
+
309
+ Union type for ads.txt entries:
310
+
311
+ ```typescript
312
+ type ParsedAdsTxtEntry = ParsedAdsTxtRecord | ParsedAdsTxtVariable;
313
+ ```
314
+
315
+ #### `ParsedAdsTxtRecord`
316
+
317
+ Interface for ads.txt records:
318
+
319
+ ```typescript
320
+ interface ParsedAdsTxtRecord {
321
+ line_number: number;
322
+ raw_line: string;
323
+ is_valid: boolean;
324
+ domain: string;
325
+ account_id: string;
326
+ account_type: string;
327
+ certification_authority_id?: string;
328
+ relationship: 'DIRECT' | 'RESELLER';
329
+ error?: string;
330
+ has_warning?: boolean;
331
+ warning?: string;
332
+ validation_key?: string;
333
+ severity?: Severity;
334
+ duplicate_domain?: string;
335
+ validation_results?: CrossCheckValidationResult;
336
+ }
337
+ ```
338
+
339
+ #### `ParsedAdsTxtVariable`
340
+
341
+ Interface for ads.txt variables:
342
+
343
+ ```typescript
344
+ interface ParsedAdsTxtVariable {
345
+ line_number: number;
346
+ raw_line: string;
347
+ is_valid: boolean;
348
+ variable_type:
349
+ | 'CONTACT'
350
+ | 'SUBDOMAIN'
351
+ | 'INVENTORYPARTNERDOMAIN'
352
+ | 'OWNERDOMAIN'
353
+ | 'MANAGERDOMAIN';
354
+ value: string;
355
+ is_variable: true;
356
+ error?: string;
357
+ has_warning?: boolean;
358
+ warning?: string;
359
+ }
360
+ ```
361
+
362
+ #### `CrossCheckValidationResult`
363
+
364
+ Detailed validation results from sellers.json cross-checking:
365
+
366
+ ```typescript
367
+ interface CrossCheckValidationResult {
368
+ hasSellerJson: boolean;
369
+ directAccountIdInSellersJson: boolean;
370
+ directDomainMatchesSellerJsonEntry: boolean | null;
371
+ directEntryHasPublisherType: boolean | null;
372
+ directSellerIdIsUnique: boolean | null;
373
+ resellerAccountIdInSellersJson: boolean | null;
374
+ resellerDomainMatchesSellerJsonEntry: boolean | null;
375
+ resellerEntryHasIntermediaryType: boolean | null;
376
+ resellerSellerIdIsUnique: boolean | null;
377
+ sellerData?: SellersJsonSellerRecord | null;
378
+ error?: string;
379
+ }
380
+ ```
381
+
382
+ #### `Severity`
383
+
384
+ Validation severity levels:
385
+
386
+ ```typescript
387
+ enum Severity {
388
+ ERROR = 'error',
389
+ WARNING = 'warning',
390
+ INFO = 'info',
391
+ }
392
+ ```
393
+
394
+ #### `MessageConfig`
395
+
396
+ Configuration interface for message system:
397
+
398
+ ```typescript
399
+ interface MessageConfig {
400
+ defaultLocale?: 'ja' | 'en';
401
+ baseUrl?: string;
402
+ }
403
+ ```
404
+
405
+ #### `ValidationMessage`
406
+
407
+ Complete validation message with localized content:
408
+
409
+ ```typescript
410
+ interface ValidationMessage {
411
+ key: string;
412
+ severity: Severity;
413
+ message: string;
414
+ description?: string;
415
+ helpUrl?: string;
416
+ placeholders: string[];
417
+ }
418
+ ```
419
+
420
+ ### Type Guards
421
+
422
+ #### `isAdsTxtRecord(entry: ParsedAdsTxtEntry): entry is ParsedAdsTxtRecord`
423
+
424
+ Checks if an entry is an ads.txt record.
425
+
426
+ #### `isAdsTxtVariable(entry: ParsedAdsTxtEntry): entry is ParsedAdsTxtVariable`
427
+
428
+ Checks if an entry is an ads.txt variable.
429
+
430
+ ## Validation Features
431
+
432
+ ### Basic Validation
433
+
434
+ - **Format validation**: Ensures proper comma-separated format
435
+ - **Required fields**: Validates presence of domain, account_id, account_type
436
+ - **Domain validation**: Uses PSL (Public Suffix List) for domain validation
437
+ - **Relationship validation**: Ensures valid DIRECT/RESELLER relationships
438
+ - **Account ID validation**: Checks for non-empty account IDs
439
+
440
+ ### Advanced Validation (Sellers.json Cross-checking)
441
+
442
+ The package implements comprehensive sellers.json validation based on IAB standards:
443
+
444
+ - **Case 11/16**: Checks if advertising system has sellers.json file
445
+ - **Case 12**: For DIRECT entries, validates account ID exists in sellers.json
446
+ - **Case 13**: For DIRECT entries, validates domain matching against OWNERDOMAIN/MANAGERDOMAIN
447
+ - **Case 14**: For DIRECT entries, validates seller_type is PUBLISHER
448
+ - **Case 15**: For DIRECT entries, validates seller_id uniqueness
449
+ - **Case 17**: For RESELLER entries, validates account ID exists in sellers.json
450
+ - **Case 18**: For RESELLER entries, validates domain matching
451
+ - **Case 19**: For RESELLER entries, validates seller_type is INTERMEDIARY
452
+ - **Case 20**: For RESELLER entries, validates seller_id uniqueness
453
+
454
+ ### Duplicate Detection
455
+
456
+ - Detects duplicate entries between submitted and existing ads.txt files
457
+ - Uses normalized comparison (case-insensitive domains, exact account IDs)
458
+ - Marks duplicates with INFO severity warnings
459
+
460
+ ## Performance Optimization
461
+
462
+ ### SellersJsonProvider vs Legacy Approach
463
+
464
+ The new `SellersJsonProvider` interface offers significant performance improvements over the legacy approach:
465
+
466
+ | Aspect | Legacy Approach | SellersJsonProvider |
467
+ | -------------------- | ---------------------------------- | ---------------------------------- |
468
+ | **Memory Usage** | Loads entire sellers.json (100MB+) | Loads only needed sellers (few KB) |
469
+ | **Network Transfer** | Downloads complete files | Selective database queries |
470
+ | **Processing Time** | O(n) linear search | O(log n) indexed lookups |
471
+ | **Scalability** | Poor for large files | Excellent for any size |
472
+
473
+ ### Performance Metrics
474
+
475
+ For a typical sellers.json file with 10,000 sellers:
476
+
477
+ - **Memory reduction**: 99%+ (100MB → 5KB)
478
+ - **Query speed**: 50-100x faster
479
+ - **Network bandwidth**: 95%+ reduction
480
+
481
+ ### When to Use Each Approach
482
+
483
+ **Use SellersJsonProvider when:**
484
+
485
+ - Working with large sellers.json files (>1MB)
486
+ - Performance is critical
487
+ - Database/cache infrastructure is available
488
+ - Processing multiple ads.txt files
489
+
490
+ **Use Legacy approach when:**
491
+
492
+ - Simple one-off validations
493
+ - No database infrastructure
494
+ - Working with small sellers.json files
495
+ - Backward compatibility is required
496
+
497
+ ## Error Handling
498
+
499
+ The package uses comprehensive error keys for different validation scenarios:
500
+
501
+ - `MISSING_FIELDS`: Missing required fields
502
+ - `INVALID_FORMAT`: Invalid line format
503
+ - `INVALID_RELATIONSHIP`: Invalid relationship type
504
+ - `INVALID_DOMAIN`: Invalid domain format
505
+ - `EMPTY_ACCOUNT_ID`: Empty account ID
506
+ - `IMPLIMENTED`: Duplicate entry detected
507
+ - `NO_SELLERS_JSON`: Missing sellers.json file
508
+ - `DIRECT_ACCOUNT_ID_NOT_IN_SELLERS_JSON`: Direct account not in sellers.json
509
+ - `RESELLER_ACCOUNT_ID_NOT_IN_SELLERS_JSON`: Reseller account not in sellers.json
510
+ - `DOMAIN_MISMATCH`: Domain mismatch with sellers.json
511
+ - `DIRECT_NOT_PUBLISHER`: Direct entry not marked as publisher
512
+ - `SELLER_ID_NOT_UNIQUE`: Seller ID appears multiple times
513
+ - `RESELLER_NOT_INTERMEDIARY`: Reseller not marked as intermediary
514
+
515
+ ## Examples
516
+
517
+ ### Basic Parsing
518
+
519
+ ```typescript
520
+ import { parseAdsTxtContent } from '@miyaichi/ads-txt-validator';
521
+
522
+ const adsTxtContent = `
523
+ # Ads.txt file
524
+ example.com, pub-1234, DIRECT, f08c47fec0942fa0
525
+ reseller.com, reseller-5678, RESELLER
526
+ CONTACT=admin@example.com
527
+ OWNERDOMAIN=example.com
528
+ `;
529
+
530
+ const parsedEntries = parseAdsTxtContent(adsTxtContent, 'example.com');
531
+
532
+ // Filter only valid records
533
+ const validRecords = parsedEntries.filter((entry) => entry.is_valid).filter(isAdsTxtRecord);
534
+
535
+ console.log(`Found ${validRecords.length} valid records`);
536
+ ```
537
+
538
+ ### Cross-checking with Sellers.json
539
+
540
+ ```typescript
541
+ import { crossCheckAdsTxtRecords, parseAdsTxtContent } from '@miyaichi/ads-txt-validator';
542
+
543
+ const getSellersJson = async (domain: string) => {
544
+ try {
545
+ const response = await fetch(`https://${domain}/sellers.json`);
546
+ return await response.json();
547
+ } catch (error) {
548
+ return null; // Handle fetch errors
549
+ }
550
+ };
551
+
552
+ const parsedEntries = parseAdsTxtContent(adsTxtContent, 'publisher.com');
553
+
554
+ const validatedEntries = await crossCheckAdsTxtRecords(
555
+ 'publisher.com',
556
+ parsedEntries,
557
+ existingAdsTxtContent,
558
+ getSellersJson
559
+ );
560
+
561
+ // Check validation results
562
+ validatedEntries.forEach((entry) => {
563
+ if (entry.has_warning) {
564
+ console.warn(`Warning for ${entry.raw_line}: ${entry.warning}`);
565
+ }
566
+ if (entry.validation_results) {
567
+ console.log('Validation details:', entry.validation_results);
568
+ }
569
+ });
570
+ ```
571
+
572
+ ### Configuring Help URLs for External Applications
573
+
574
+ When using this package as an external library, you can configure the base URL for help links:
575
+
576
+ ```typescript
577
+ import { configureMessages, createValidationMessage } from '@miyaichi/ads-txt-validator';
578
+
579
+ // Configure the message system with your application's base URL
580
+ configureMessages({
581
+ defaultLocale: 'ja', // or 'en'
582
+ baseUrl: 'https://your-app.com',
583
+ });
584
+
585
+ // Now validation messages will have complete URLs
586
+ const message = createValidationMessage('domainMismatch', ['example.com', 'google.com']);
587
+ console.log(message.helpUrl);
588
+ // Output: https://your-app.com/help/#domain-mismatch
589
+
590
+ // For deployment environments, use environment variables
591
+ if (process.env.APP_URL) {
592
+ configureMessages({
593
+ defaultLocale: 'ja',
594
+ baseUrl: process.env.APP_URL,
595
+ });
596
+ }
597
+ ```
598
+
599
+ #### Help URL Generation
600
+
601
+ The package automatically generates help URLs based on validation keys using the following format:
602
+
603
+ ```
604
+ {baseUrl}/help/#{validation-key}
605
+ ```
606
+
607
+ **Validation Key to URL Fragment Mapping:**
608
+
609
+ | Validation Key | URL Fragment | Description |
610
+ |----------------|--------------|-------------|
611
+ | `missingFields` | `missing-fields` | Missing required fields error |
612
+ | `invalidFormat` | `invalid-format` | Invalid line format error |
613
+ | `invalidRelationship` | `invalid-relationship` | Invalid relationship type |
614
+ | `invalidDomain` | `invalid-domain` | Invalid domain format |
615
+ | `emptyAccountId` | `empty-account-id` | Empty account ID error |
616
+ | `noValidEntries` | `no-valid-entries` | No valid entries found |
617
+ | `whitespaceInFields` | `whitespace-in-fields` | Whitespace in fields error |
618
+ | `implimented` | `implimented-entry` | Duplicate entry warning |
619
+ | `noSellersJson` | `no-sellers-json` | Missing sellers.json file |
620
+ | `directAccountIdNotInSellersJson` | `direct-account-id-not-in-sellers-json` | Direct account not in sellers.json |
621
+ | `resellerAccountIdNotInSellersJson` | `reseller-account-id-not-in-sellers-json` | Reseller account not in sellers.json |
622
+ | `domainMismatch` | `domain-mismatch` | Domain mismatch with sellers.json |
623
+ | `directNotPublisher` | `direct-not-publisher` | Direct entry not marked as publisher |
624
+ | `sellerIdNotUnique` | `seller-id-not-unique` | Seller ID not unique |
625
+ | `resellerNotIntermediary` | `reseller-not-intermediary` | Reseller not marked as intermediary |
626
+
627
+ #### Implementing Help Pages
628
+
629
+ To implement help pages in your application, create a help page that supports URL fragments:
630
+
631
+ ```typescript
632
+ // Example React component for help page
633
+ import React, { useEffect } from 'react';
634
+ import { useLocation } from 'react-router-dom';
635
+
636
+ export const HelpPage: React.FC = () => {
637
+ const location = useLocation();
638
+
639
+ useEffect(() => {
640
+ // Highlight section based on URL fragment
641
+ const fragment = location.hash.replace('#', '');
642
+ if (fragment) {
643
+ const element = document.getElementById(fragment);
644
+ if (element) {
645
+ element.scrollIntoView({ behavior: 'smooth' });
646
+ // Add highlight effect
647
+ element.classList.add('highlight-section');
648
+ setTimeout(() => {
649
+ element.classList.remove('highlight-section');
650
+ }, 5000);
651
+ }
652
+ }
653
+ }, [location.hash]);
654
+
655
+ return (
656
+ <div>
657
+ <h1>Ads.txt Validation Help</h1>
658
+
659
+ <section id="domain-mismatch">
660
+ <h2>Domain Mismatch</h2>
661
+ <p>This error occurs when the domain in sellers.json doesn't match...</p>
662
+ </section>
663
+
664
+ <section id="missing-fields">
665
+ <h2>Missing Fields</h2>
666
+ <p>Ads.txt entries require at least three fields...</p>
667
+ </section>
668
+
669
+ {/* Add more sections for each validation key */}
670
+ </div>
671
+ );
672
+ };
673
+ ```
674
+
675
+ #### Static Help Page Example
676
+
677
+ For static HTML applications, you can implement fragment-based navigation:
678
+
679
+ ```html
680
+ <!DOCTYPE html>
681
+ <html>
682
+ <head>
683
+ <title>Ads.txt Validation Help</title>
684
+ <style>
685
+ .highlight-section {
686
+ background-color: rgba(255, 193, 7, 0.3);
687
+ border-radius: 4px;
688
+ padding: 8px;
689
+ margin: -8px;
690
+ transition: all 0.3s ease-in-out;
691
+ animation: highlight-fade 5s ease-out forwards;
692
+ }
693
+
694
+ @keyframes highlight-fade {
695
+ 0% { background-color: rgba(255, 193, 7, 0.4); }
696
+ 100% { background-color: transparent; }
697
+ }
698
+ </style>
699
+ </head>
700
+ <body>
701
+ <h1>Ads.txt Validation Help</h1>
702
+
703
+ <section>
704
+ <a id="domain-mismatch"></a>
705
+ <h2>Domain Mismatch</h2>
706
+ <p>This error occurs when...</p>
707
+ </section>
708
+
709
+ <script>
710
+ // Handle fragment highlighting
711
+ function highlightSection() {
712
+ const fragment = window.location.hash.replace('#', '');
713
+ if (fragment) {
714
+ const element = document.getElementById(fragment);
715
+ if (element) {
716
+ element.scrollIntoView({ behavior: 'smooth' });
717
+
718
+ // If it's an empty anchor, highlight the next heading
719
+ let elementToHighlight = element;
720
+ if (element.tagName === 'A' && !element.textContent.trim()) {
721
+ const nextHeading = element.nextElementSibling;
722
+ if (nextHeading && nextHeading.tagName.match(/^H[1-6]$/)) {
723
+ elementToHighlight = nextHeading;
724
+ }
725
+ }
726
+
727
+ elementToHighlight.classList.add('highlight-section');
728
+ setTimeout(() => {
729
+ elementToHighlight.classList.remove('highlight-section');
730
+ }, 5000);
731
+ }
732
+ }
733
+ }
734
+
735
+ // Highlight on page load and hash change
736
+ window.addEventListener('load', highlightSection);
737
+ window.addEventListener('hashchange', highlightSection);
738
+ </script>
739
+ </body>
740
+ </html>
741
+ ```
742
+
743
+ ### Content Optimization
744
+
745
+ ```typescript
746
+ import { optimizeAdsTxt } from '@miyaichi/ads-txt-validator';
747
+
748
+ const messyAdsTxtContent = `
749
+ # Ads.txt file
750
+ example.com, pub-1234, DIRECT
751
+ example.com, pub-1234, DIRECT
752
+ CONTACT=admin@example.com
753
+ reseller.com, reseller-5678, RESELLER
754
+ CONTACT=admin@example.com
755
+ `;
756
+
757
+ const optimizedContent = optimizeAdsTxt(messyAdsTxtContent, 'publisher.com');
758
+ console.log(optimizedContent);
759
+ // Output will have duplicates removed and content organized
760
+ ```
761
+
762
+ ### Error Handling
763
+
764
+ ```typescript
765
+ import { parseAdsTxtContent, isAdsTxtRecord } from '@miyaichi/ads-txt-validator';
766
+
767
+ const parsedEntries = parseAdsTxtContent(adsTxtContent);
768
+
769
+ parsedEntries.forEach((entry) => {
770
+ if (!entry.is_valid) {
771
+ console.error(`Line ${entry.line_number}: ${entry.error}`);
772
+ }
773
+
774
+ if (entry.has_warning) {
775
+ console.warn(`Line ${entry.line_number}: ${entry.warning}`);
776
+ }
777
+
778
+ if (isAdsTxtRecord(entry) && entry.validation_results) {
779
+ if (!entry.validation_results.hasSellerJson) {
780
+ console.warn(`No sellers.json found for ${entry.domain}`);
781
+ }
782
+ }
783
+ });
784
+ ```
785
+
786
+ ## Dependencies
787
+
788
+ - `psl`: Public Suffix List for domain validation
789
+
790
+ ## Development
791
+
792
+ ```bash
793
+ # Build the package
794
+ npm run build
795
+
796
+ # Run tests
797
+ npm test
798
+ ```
799
+
800
+ ## License
801
+
802
+ MIT
803
+
804
+ ## Contributing
805
+
806
+ Contributions are welcome! Please ensure all tests pass and follow the existing code style.