@@ -211,6 +211,104 @@ function getLinkText(node, children, settings) {
211211 return href ? `[${ trimmedContent } ](${ href } )` : collapseWhitespace ( children ) ;
212212}
213213
214+ /**
215+ * Extract cheap, normalized page-type signals from a document: schema.org JSON-LD `@type`,
216+ * Open Graph `og:type`, and the declared page language. Every field is best-effort and is empty
217+ * or null when the page doesn't expose it. Domain is intentionally omitted; consumers derive it
218+ * from the page URL that already accompanies the collected content.
219+ * @param {Document } document
220+ * @param {{ maxTypes?: number, maxBlockLength?: number } } [options]
221+ * @returns {{ jsonLdType: string[], ogType: string | null, lang: string } }
222+ */
223+ export function extractPageTypeSignals ( document , { maxTypes = 10 , maxBlockLength = 100000 } = { } ) {
224+ return {
225+ jsonLdType : extractJsonLdTypes ( document , maxTypes , maxBlockLength ) ,
226+ ogType : extractOgType ( document ) ,
227+ lang : extractLang ( document ) ,
228+ } ;
229+ }
230+
231+ /**
232+ * Collect deduped schema.org `@type` values from every JSON-LD block, preserving casing. Handles
233+ * `@type` as a string or array and entries nested under `@graph`. Malformed and oversized blocks
234+ * are skipped so a bad block never fails the whole collection.
235+ * @param {Document } document
236+ * @param {number } maxTypes
237+ * @param {number } maxBlockLength
238+ * @returns {string[] }
239+ */
240+ function extractJsonLdTypes ( document , maxTypes , maxBlockLength ) {
241+ /** @type {string[] } */
242+ const types = [ ] ;
243+ const seen = new Set ( ) ;
244+ /** @param {unknown } value */
245+ const add = ( value ) => {
246+ if ( typeof value !== 'string' ) return ;
247+ const type = value . trim ( ) ;
248+ if ( ! type || seen . has ( type ) ) return ;
249+ seen . add ( type ) ;
250+ types . push ( type ) ;
251+ } ;
252+
253+ const blocks = document . querySelectorAll ( 'script[type="application/ld+json"]' ) ;
254+ for ( const block of blocks ) {
255+ if ( types . length >= maxTypes ) break ;
256+ const text = block . textContent || '' ;
257+ if ( ! text || text . length > maxBlockLength ) continue ;
258+ let data ;
259+ try {
260+ data = JSON . parse ( text ) ;
261+ } catch ( e ) {
262+ continue ;
263+ }
264+ collectJsonLdTypes ( data , add ) ;
265+ }
266+
267+ return types . slice ( 0 , maxTypes ) ;
268+ }
269+
270+ /**
271+ * Walk a parsed JSON-LD value (object, array, or `@graph` container) and report each `@type`.
272+ * @param {unknown } node
273+ * @param {(value: unknown) => void } add
274+ */
275+ function collectJsonLdTypes ( node , add ) {
276+ if ( Array . isArray ( node ) ) {
277+ for ( const item of node ) collectJsonLdTypes ( item , add ) ;
278+ return ;
279+ }
280+ if ( ! node || typeof node !== 'object' ) return ;
281+ const obj = /** @type {Record<string, unknown> } */ ( node ) ;
282+ const type = obj [ '@type' ] ;
283+ if ( Array . isArray ( type ) ) {
284+ for ( const value of type ) add ( value ) ;
285+ } else if ( type != null ) {
286+ add ( type ) ;
287+ }
288+ if ( obj [ '@graph' ] ) {
289+ collectJsonLdTypes ( obj [ '@graph' ] , add ) ;
290+ }
291+ }
292+
293+ /**
294+ * @param {Document } document
295+ * @returns {string | null }
296+ */
297+ function extractOgType ( document ) {
298+ const meta = document . querySelector ( 'meta[property="og:type"]' ) ;
299+ const content = meta ?. getAttribute ( 'content' ) ;
300+ const trimmed = typeof content === 'string' ? content . trim ( ) : '' ;
301+ return trimmed || null ;
302+ }
303+
304+ /**
305+ * @param {Document } document
306+ * @returns {string }
307+ */
308+ function extractLang ( document ) {
309+ return ( document . documentElement ?. getAttribute ( 'lang' ) || '' ) . trim ( ) ;
310+ }
311+
214312export default class PageContext extends ContentFeature {
215313 /** @type {any } */
216314 #cachedContent = undefined ;
@@ -492,6 +590,9 @@ export default class PageContext extends ContentFeature {
492590 if ( this . getFeatureSettingEnabled ( 'includeImages' , 'disabled' ) ) {
493591 content . images = this . getImages ( ) ;
494592 }
593+ if ( this . getFeatureSettingEnabled ( 'includePageTypeSignals' , 'disabled' ) ) {
594+ content . pageTypeSignals = this . getPageTypeSignals ( ) ;
595+ }
495596
496597 // Cache the result - setter handles timestamp and observer
497598 // Note: We only cache if content exists. Consider caching empty content too
@@ -620,6 +721,13 @@ export default class PageContext extends ContentFeature {
620721 return links ;
621722 }
622723
724+ getPageTypeSignals ( ) {
725+ return extractPageTypeSignals ( document , {
726+ maxTypes : this . getFeatureSetting ( 'maxPageTypeSignals' ) || 10 ,
727+ maxBlockLength : this . getFeatureSetting ( 'maxJsonLdBlockLength' ) || 100000 ,
728+ } ) ;
729+ }
730+
623731 getImages ( ) {
624732 const images = [ ] ;
625733 const imgSelector = this . getFeatureSetting ( 'imgSelector' ) || 'img' ;
0 commit comments