@nitpicker/crawler 0.13.0 → 0.14.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.
Files changed (71) hide show
  1. package/lib/archive/archive-accessor.d.ts +56 -0
  2. package/lib/archive/archive-accessor.js +72 -0
  3. package/lib/archive/archive.d.ts +11 -0
  4. package/lib/archive/archive.js +13 -0
  5. package/lib/archive/create-adjunct-tables.d.ts +7 -0
  6. package/lib/archive/create-adjunct-tables.js +150 -0
  7. package/lib/archive/create-entity-tables.d.ts +14 -0
  8. package/lib/archive/create-entity-tables.js +32 -1
  9. package/lib/archive/database.d.ts +55 -1
  10. package/lib/archive/database.js +81 -0
  11. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +20 -0
  12. package/lib/archive/db-ops/analysis/replace-page-templates.js +52 -0
  13. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  14. package/lib/archive/db-ops/lifecycle/init.d.ts +16 -10
  15. package/lib/archive/db-ops/lifecycle/init.js +20 -10
  16. package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
  17. package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
  18. package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
  19. package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
  20. package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
  21. package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
  22. package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
  23. package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
  24. package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
  25. package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
  26. package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
  27. package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
  28. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
  29. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
  30. package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
  31. package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
  32. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  33. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
  34. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +5 -3
  35. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +13 -3
  36. package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
  37. package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
  38. package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
  39. package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
  40. package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
  41. package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
  42. package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
  43. package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
  44. package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
  45. package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
  46. package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
  47. package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
  48. package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
  49. package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
  50. package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
  51. package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
  52. package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
  53. package/lib/archive/db-ops/pages/write/update-page.js +21 -0
  54. package/lib/archive/init-schema.js +2 -1
  55. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
  56. package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
  57. package/lib/archive/meta/types.d.ts +263 -0
  58. package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
  59. package/lib/archive/migrate-info-main-content-selector.js +24 -0
  60. package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
  61. package/lib/archive/migrate-main-contents-columns.js +50 -0
  62. package/lib/archive/page.d.ts +144 -2
  63. package/lib/archive/page.js +195 -0
  64. package/lib/archive/types.d.ts +39 -0
  65. package/lib/crawler/crawler.js +4 -2
  66. package/lib/crawler/fetch-destination.js +2 -0
  67. package/lib/crawler/link-to-page-data.js +2 -0
  68. package/lib/crawler/resource-to-page-data.js +2 -0
  69. package/lib/crawler/types.d.ts +5 -0
  70. package/lib/crawler-orchestrator.js +2 -0
  71. package/package.json +3 -3
@@ -38,9 +38,11 @@ import { getFailedPageMessages } from '../../../get-failed-page-messages.js';
38
38
  * The page row itself is kept (id preserved) so existing
39
39
  * `anchor_edges.href_page_id` referrers stay valid, and `is_external` is
40
40
  * left untouched so the next pass re-classifies each page from the crawl
41
- * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`, and
42
- * `page_errors` rows are deleted so the re-scrape can re-insert fresh data
43
- * without duplicates.
41
+ * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`,
42
+ * `page_errors`, and the `page_main_content_*` child tables are deleted so
43
+ * the re-scrape can re-insert fresh data without duplicates — kept in sync
44
+ * with the `page_meta` row deletion above so a reset page's `main_content_*`
45
+ * counts and its child-table detail never disagree.
44
46
  *
45
47
  * SELECT and UPDATE/DELETE statements are chunked to stay below SQLite's
46
48
  * `SQLITE_LIMIT_VARIABLE_NUMBER`.
@@ -118,6 +120,14 @@ export async function resetFailedPages(knex) {
118
120
  await knex('page_html_ref').whereIn('page_id', chunk).delete();
119
121
  await knex('page_tags').whereIn('pageId', chunk).delete();
120
122
  await knex('page_jsonld').whereIn('pageId', chunk).delete();
123
+ await knex('page_main_content_headings').whereIn('pageId', chunk).delete();
124
+ await knex('page_main_content_images').whereIn('pageId', chunk).delete();
125
+ await knex('page_main_content_tables').whereIn('pageId', chunk).delete();
126
+ await knex('page_main_content_buttons').whereIn('pageId', chunk).delete();
127
+ await knex('page_main_content_iframes').whereIn('pageId', chunk).delete();
128
+ await knex('page_main_content_videos').whereIn('pageId', chunk).delete();
129
+ await knex('page_main_content_audios').whereIn('pageId', chunk).delete();
130
+ await knex('page_main_content_canvases').whereIn('pageId', chunk).delete();
121
131
  }
122
132
  dbLog('Reset %d failed pages back to pending', urls.length);
123
133
  return urls;
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_audios` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertAudios(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_audios` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertAudios(pageId, mainContents, trx) {
14
+ if (mainContents.audios.length === 0)
15
+ return;
16
+ const rows = mainContents.audios.map((audio, order) => ({
17
+ pageId,
18
+ order,
19
+ src: audio.src,
20
+ }));
21
+ await trx('page_main_content_audios').where('pageId', pageId).delete();
22
+ await eachSplitted(rows, 100, async (chunk) => {
23
+ await trx('page_main_content_audios').insert(chunk);
24
+ });
25
+ }
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_buttons` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertButtons(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_buttons` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertButtons(pageId, mainContents, trx) {
14
+ if (mainContents.buttons.length === 0)
15
+ return;
16
+ const rows = mainContents.buttons.map((button, order) => ({
17
+ pageId,
18
+ order,
19
+ nodeName: button.nodeName,
20
+ role: button.role,
21
+ type: button.type,
22
+ text: button.text,
23
+ disabled: button.disabled,
24
+ }));
25
+ await trx('page_main_content_buttons').where('pageId', pageId).delete();
26
+ await eachSplitted(rows, 100, async (chunk) => {
27
+ await trx('page_main_content_buttons').insert(chunk);
28
+ });
29
+ }
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_canvases` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertCanvases(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_canvases` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertCanvases(pageId, mainContents, trx) {
14
+ if (mainContents.canvases.length === 0)
15
+ return;
16
+ const rows = mainContents.canvases.map((canvas, order) => ({
17
+ pageId,
18
+ order,
19
+ width: canvas.width,
20
+ height: canvas.height,
21
+ }));
22
+ await trx('page_main_content_canvases').where('pageId', pageId).delete();
23
+ await eachSplitted(rows, 100, async (chunk) => {
24
+ await trx('page_main_content_canvases').insert(chunk);
25
+ });
26
+ }
@@ -0,0 +1,15 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_headings` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null` (non-HTML / external / metadata-only scrapes have
7
+ * no main-content data to write).
8
+ *
9
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
10
+ * wipe prior rows on a degraded re-scrape.
11
+ * @param pageId - The owning `content_items.id`.
12
+ * @param mainContents - Beholder's per-page main-content metrics.
13
+ * @param trx - The active transaction.
14
+ */
15
+ export declare function insertHeadings(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_headings` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null` (non-HTML / external / metadata-only scrapes have
6
+ * no main-content data to write).
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export async function insertHeadings(pageId, mainContents, trx) {
15
+ if (mainContents.headings.length === 0)
16
+ return;
17
+ const rows = mainContents.headings.map((heading, order) => ({
18
+ pageId,
19
+ order,
20
+ text: heading.text,
21
+ level: heading.level,
22
+ }));
23
+ await trx('page_main_content_headings').where('pageId', pageId).delete();
24
+ await eachSplitted(rows, 100, async (chunk) => {
25
+ await trx('page_main_content_headings').insert(chunk);
26
+ });
27
+ }
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_iframes` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertIframes(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_iframes` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertIframes(pageId, mainContents, trx) {
14
+ if (mainContents.iframes.length === 0)
15
+ return;
16
+ const rows = mainContents.iframes.map((iframe, order) => ({
17
+ pageId,
18
+ order,
19
+ src: iframe.src,
20
+ title: iframe.title,
21
+ width: iframe.width,
22
+ height: iframe.height,
23
+ }));
24
+ await trx('page_main_content_iframes').where('pageId', pageId).delete();
25
+ await eachSplitted(rows, 100, async (chunk) => {
26
+ await trx('page_main_content_iframes').insert(chunk);
27
+ });
28
+ }
@@ -0,0 +1,20 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_images` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Distinct from `image_items` (the whole-page image scan, `replaceImageItems`):
9
+ * this table only covers images inside the detected main-content region, so
10
+ * `src` is stored as a plain string rather than routed through
11
+ * `url_refs` / `blob_refs` — same low-cross-page-reuse rationale as the
12
+ * `page_meta.main_content_*` columns.
13
+ *
14
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
15
+ * wipe prior rows on a degraded re-scrape.
16
+ * @param pageId - The owning `content_items.id`.
17
+ * @param mainContents - Beholder's per-page main-content metrics.
18
+ * @param trx - The active transaction.
19
+ */
20
+ export declare function insertMainContentImages(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_images` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Distinct from `image_items` (the whole-page image scan, `replaceImageItems`):
8
+ * this table only covers images inside the detected main-content region, so
9
+ * `src` is stored as a plain string rather than routed through
10
+ * `url_refs` / `blob_refs` — same low-cross-page-reuse rationale as the
11
+ * `page_meta.main_content_*` columns.
12
+ *
13
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
14
+ * wipe prior rows on a degraded re-scrape.
15
+ * @param pageId - The owning `content_items.id`.
16
+ * @param mainContents - Beholder's per-page main-content metrics.
17
+ * @param trx - The active transaction.
18
+ */
19
+ export async function insertMainContentImages(pageId, mainContents, trx) {
20
+ if (mainContents.images.length === 0)
21
+ return;
22
+ const rows = mainContents.images.map((image, order) => ({
23
+ pageId,
24
+ order,
25
+ src: image.src,
26
+ alt: image.alt,
27
+ }));
28
+ await trx('page_main_content_images').where('pageId', pageId).delete();
29
+ await eachSplitted(rows, 100, async (chunk) => {
30
+ await trx('page_main_content_images').insert(chunk);
31
+ });
32
+ }
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_tables` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertMainContentTables(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_tables` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertMainContentTables(pageId, mainContents, trx) {
14
+ if (mainContents.tables.length === 0)
15
+ return;
16
+ const rows = mainContents.tables.map((table, order) => ({
17
+ pageId,
18
+ order,
19
+ rows: table.rows,
20
+ cols: table.cols,
21
+ hasHeader: table.hasHeader,
22
+ hasFooter: table.hasFooter,
23
+ hasMergedCell: table.hasMergedCell,
24
+ }));
25
+ await trx('page_main_content_tables').where('pageId', pageId).delete();
26
+ await eachSplitted(rows, 100, async (chunk) => {
27
+ await trx('page_main_content_tables').insert(chunk);
28
+ });
29
+ }
@@ -1,4 +1,5 @@
1
1
  import { normalizeContentType } from '../../../../crawler/normalize-content-type.js';
2
+ import { computeMainContentsDenormalized } from '../../../meta/compute-main-contents-denormalized.js';
2
3
  import { computePageDenormalized } from '../../../meta/compute-page-denormalized.js';
3
4
  import { deriveFlatFromMeta } from '../../../meta/derive-flat-from-meta.js';
4
5
  import { deriveMetaExtras } from '../../../meta/derive-meta-extras.js';
@@ -46,6 +47,7 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
46
47
  const pageId = await resolveContentItemId(qb, caches, page.url.withoutHashAndAuth, undefined, source);
47
48
  const flat = deriveFlatFromMeta(page.meta, page.url.href);
48
49
  const denorm = computePageDenormalized(page.meta);
50
+ const mainContentsDenorm = computeMainContentsDenormalized(page.mainContents, page.scrollHeight);
49
51
  const extras = deriveMetaExtras(page.meta);
50
52
  const now = Date.now();
51
53
  // Canonicalize so the stored dictionary value matches the exact-string
@@ -112,7 +114,7 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
112
114
  entry.source = source;
113
115
  }
114
116
  }
115
- await upsertPageMeta(qb, caches, pageId, flat, denorm, extras);
117
+ await upsertPageMeta(qb, caches, pageId, flat, denorm, page.mainContents == null ? null : mainContentsDenorm, extras);
116
118
  return pageId;
117
119
  }
118
120
  /**
@@ -125,9 +127,16 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
125
127
  * @param pageId - The owning `content_items.id`.
126
128
  * @param flat - Flat meta columns from `deriveFlatFromMeta`.
127
129
  * @param denorm - Denormalised aggregates from `computePageDenormalized`.
130
+ * @param mainContentsDenorm - Denormalised `main_content_*` / `scroll_height_*`
131
+ * aggregates from `computeMainContentsDenormalized`, or `null` when the
132
+ * page's `mainContents` was `null` (degraded / non-HTML scrape). `null`
133
+ * here omits all seventeen columns from the upsert entirely — on
134
+ * `ON CONFLICT DO UPDATE` this leaves a previous full scrape's values
135
+ * untouched rather than overwriting them with NULL, mirroring the
136
+ * `page_main_content_*` child-table writers' same no-op-on-null guard.
128
137
  * @param extras - Nested-Meta catch-all from `deriveMetaExtras`.
129
138
  */
130
- async function upsertPageMeta(qb, caches, pageId, flat, denorm, extras) {
139
+ async function upsertPageMeta(qb, caches, pageId, flat, denorm, mainContentsDenorm, extras) {
131
140
  const texts = new Set();
132
141
  for (const { source } of PAGE_META_COLUMN_MAPS.text) {
133
142
  const value = flat[source];
@@ -174,6 +183,10 @@ async function upsertPageMeta(qb, caches, pageId, flat, denorm, extras) {
174
183
  tag_count: denorm.tag_count,
175
184
  jsonld_count: denorm.jsonld_count,
176
185
  tags_providers_csv: denorm.tags_providers_csv,
186
+ // Omitted entirely (not set to null) when mainContentsDenorm is null —
187
+ // see the parameter doc above for why this preserves prior values on
188
+ // ON CONFLICT DO UPDATE instead of erasing them.
189
+ ...mainContentsDenorm,
177
190
  };
178
191
  for (const { source, target } of PAGE_META_COLUMN_MAPS.text) {
179
192
  const value = flat[source];
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_videos` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertVideos(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_videos` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertVideos(pageId, mainContents, trx) {
14
+ if (mainContents.videos.length === 0)
15
+ return;
16
+ const rows = mainContents.videos.map((video, order) => ({
17
+ pageId,
18
+ order,
19
+ src: video.src,
20
+ poster: video.poster,
21
+ width: video.width,
22
+ height: video.height,
23
+ }));
24
+ await trx('page_main_content_videos').where('pageId', pageId).delete();
25
+ await eachSplitted(rows, 100, async (chunk) => {
26
+ await trx('page_main_content_videos').insert(chunk);
27
+ });
28
+ }
@@ -9,9 +9,17 @@ import { resolveRedirectChain } from '../../../resolve-redirect-chain.js';
9
9
  import { clearWriteRefCaches } from '../../_shared/clear-write-ref-caches.js';
10
10
  import { resolveContentItemId } from '../../_shared/resolve-content-item-id.js';
11
11
  import { resolveUrlOrBlob } from '../../_shared/resolve-url-or-blob.js';
12
+ import { insertAudios } from './insert-audios.js';
13
+ import { insertButtons } from './insert-buttons.js';
14
+ import { insertCanvases } from './insert-canvases.js';
15
+ import { insertHeadings } from './insert-headings.js';
16
+ import { insertIframes } from './insert-iframes.js';
12
17
  import { insertJsonLd } from './insert-jsonld.js';
18
+ import { insertMainContentImages } from './insert-main-content-images.js';
19
+ import { insertMainContentTables } from './insert-main-content-tables.js';
13
20
  import { insertPage } from './insert-page.js';
14
21
  import { insertTags } from './insert-tags.js';
22
+ import { insertVideos } from './insert-videos.js';
15
23
  import { linkRedirectSources } from './link-redirect-sources.js';
16
24
  import { writePageHtmlBlob } from './write-page-html-blob.js';
17
25
  /**
@@ -92,6 +100,19 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
92
100
  if (writeHtml) {
93
101
  await insertJsonLd(pageId, page.meta, trx);
94
102
  }
103
+ // beholder's MainContentsData is captured in the same render pass as the
104
+ // HTML body / JSON-LD, so it shares the same `writeHtml` + non-null gate:
105
+ // external / non-HTML / metadata-only scrapes never populate it.
106
+ if (writeHtml && page.mainContents) {
107
+ await insertHeadings(pageId, page.mainContents, trx);
108
+ await insertMainContentImages(pageId, page.mainContents, trx);
109
+ await insertMainContentTables(pageId, page.mainContents, trx);
110
+ await insertButtons(pageId, page.mainContents, trx);
111
+ await insertIframes(pageId, page.mainContents, trx);
112
+ await insertVideos(pageId, page.mainContents, trx);
113
+ await insertAudios(pageId, page.mainContents, trx);
114
+ await insertCanvases(pageId, page.mainContents, trx);
115
+ }
95
116
  // Chain lineage propagates FROM the originating URL
96
117
  // (`page.url`), NOT from the destination. See the matching
97
118
  // rationale in `recordRedirect`: intermediates are reached
@@ -14,7 +14,7 @@ import { createRefTables } from './create-ref-tables.js';
14
14
  export async function applyConnectionPragmas(instance) {
15
15
  // Foreign-key enforcement defaults to OFF on every new SQLite
16
16
  // connection. Required for ON DELETE CASCADE on `page_html_ref`,
17
- // `page_tags`, and `page_jsonld` to fire.
17
+ // `page_tags`, `page_jsonld`, and the `page_main_content_*` tables to fire.
18
18
  await instance.raw('PRAGMA foreign_keys = ON');
19
19
  await instance.raw('PRAGMA wal_autocheckpoint = 1000');
20
20
  // Negative value = KiB of memory (64 MiB). Helps large BLOB scans.
@@ -111,6 +111,7 @@ export async function initSchema(instance) {
111
111
  t.boolean('disableQueries');
112
112
  t.string('userAgent');
113
113
  t.boolean('ignoreRobots');
114
+ t.string('mainContentSelector');
114
115
  });
115
116
  }
116
117
  // 0.13 ref / header dictionary tables. DDL + column-level rationale
@@ -0,0 +1,22 @@
1
+ import type { MainContentsDenormalizedColumns } from './types.js';
2
+ import type { MainContentsData, ScrollHeightData } from '@d-zero/beholder';
3
+ /**
4
+ * Computes the `page_meta.main_content_*` / `scroll_height_*` denormalised
5
+ * aggregate columns from beholder's `MainContentsData` / `ScrollHeightData`.
6
+ *
7
+ * `mainContents` is `null` for pages that were not fully rendered (external,
8
+ * non-HTML, or metadata-only scrapes) — in that case every column is `null`.
9
+ * This mirrors {@link import('./compute-page-denormalized.js').computePageDenormalized}'s
10
+ * write-once-at-scrape-time pattern so list / detail reads never re-derive
11
+ * counts from the `page_main_content_*` child tables.
12
+ *
13
+ * Accepts `undefined` as well as `null`: `PageData` declares `mainContents`
14
+ * as required, but test fixtures across the codebase predate this field and
15
+ * omit it (`.spec.ts` files are excluded from the `tsc` build, so this goes
16
+ * uncaught at compile time) — `== null` tolerates both without forcing every
17
+ * fixture to be updated.
18
+ * @param mainContents - Beholder's per-page main-content metrics, or `null`/`undefined`.
19
+ * @param scrollHeight - Beholder's per-page scroll-height measurements, or `null`/`undefined`.
20
+ * @returns The seventeen denormalised columns.
21
+ */
22
+ export declare function computeMainContentsDenormalized(mainContents: MainContentsData | null | undefined, scrollHeight: ScrollHeightData | null | undefined): MainContentsDenormalizedColumns;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Computes the `page_meta.main_content_*` / `scroll_height_*` denormalised
3
+ * aggregate columns from beholder's `MainContentsData` / `ScrollHeightData`.
4
+ *
5
+ * `mainContents` is `null` for pages that were not fully rendered (external,
6
+ * non-HTML, or metadata-only scrapes) — in that case every column is `null`.
7
+ * This mirrors {@link import('./compute-page-denormalized.js').computePageDenormalized}'s
8
+ * write-once-at-scrape-time pattern so list / detail reads never re-derive
9
+ * counts from the `page_main_content_*` child tables.
10
+ *
11
+ * Accepts `undefined` as well as `null`: `PageData` declares `mainContents`
12
+ * as required, but test fixtures across the codebase predate this field and
13
+ * omit it (`.spec.ts` files are excluded from the `tsc` build, so this goes
14
+ * uncaught at compile time) — `== null` tolerates both without forcing every
15
+ * fixture to be updated.
16
+ * @param mainContents - Beholder's per-page main-content metrics, or `null`/`undefined`.
17
+ * @param scrollHeight - Beholder's per-page scroll-height measurements, or `null`/`undefined`.
18
+ * @returns The seventeen denormalised columns.
19
+ */
20
+ export function computeMainContentsDenormalized(mainContents, scrollHeight) {
21
+ if (mainContents == null) {
22
+ return {
23
+ main_content_node_name: null,
24
+ main_content_id: null,
25
+ main_content_role: null,
26
+ main_content_selector: null,
27
+ main_content_class_list: null,
28
+ main_content_word_count: null,
29
+ main_content_body_word_count: null,
30
+ main_content_heading_count: null,
31
+ main_content_image_count: null,
32
+ main_content_table_count: null,
33
+ main_content_button_count: null,
34
+ main_content_iframe_count: null,
35
+ main_content_video_count: null,
36
+ main_content_audio_count: null,
37
+ main_content_canvas_count: null,
38
+ scroll_height_desktop: null,
39
+ scroll_height_mobile: null,
40
+ };
41
+ }
42
+ return {
43
+ main_content_node_name: mainContents.main?.nodeName ?? null,
44
+ main_content_id: mainContents.main?.id ?? null,
45
+ main_content_role: mainContents.main?.role ?? null,
46
+ main_content_selector: mainContents.main?.selector ?? null,
47
+ main_content_class_list: mainContents.main
48
+ ? JSON.stringify(mainContents.main.classList)
49
+ : null,
50
+ main_content_word_count: mainContents.wordCount,
51
+ main_content_body_word_count: mainContents.bodyWordCount,
52
+ main_content_heading_count: mainContents.headings.length,
53
+ main_content_image_count: mainContents.images.length,
54
+ main_content_table_count: mainContents.tables.length,
55
+ main_content_button_count: mainContents.buttons.length,
56
+ main_content_iframe_count: mainContents.iframes.length,
57
+ main_content_video_count: mainContents.videos.length,
58
+ main_content_audio_count: mainContents.audios.length,
59
+ main_content_canvas_count: mainContents.canvases.length,
60
+ scroll_height_desktop: scrollHeight?.desktop ?? null,
61
+ scroll_height_mobile: scrollHeight?.mobile ?? null,
62
+ };
63
+ }