page_structured_data 1.0.13 → 1.0.14
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +230 -3
- data/app/src/page_structured_data/page.rb +27 -2
- data/app/src/page_structured_data/page_types/article.rb +54 -2
- data/app/src/page_structured_data/page_types/interaction_statistic.rb +7 -0
- data/app/src/page_structured_data/page_types/organization.rb +13 -0
- data/app/src/page_structured_data/page_types/person.rb +4 -0
- data/app/src/page_structured_data/page_types/schema_node.rb +14 -0
- data/app/src/page_structured_data/page_types/web_site.rb +11 -0
- data/app/views/page_structured_data/_meta_tags.html.erb +4 -0
- data/lib/page_structured_data/version.rb +1 -1
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e36ab494ebd4db8743deb6d8415e826c2186f7c28a7e2927c8a08df4decabd6
|
|
4
|
+
data.tar.gz: b48c8723a6ad4a3ee5aa44be5e6b36d6764abbd433a3daedb1e3278f32cf7b2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c9dd473ced4959c0acc17940b212d0c71b760efe3a0cfe119d74845ab10b76e081363b26d4138f6b5d66a5e62d0f7c36623c5603ac78ec8565350e2cd53dbcf
|
|
7
|
+
data.tar.gz: eefd736852c02d3ec1f02a58e7911f931d8882ce09854364c9b028cd1a11aa1872c3fe1a79beb4c379b8468e1b3db7871a00331dc47522573da9abc64bfa6dce
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.0.14 - 2026-07-05
|
|
8
|
+
|
|
9
|
+
- Add page-level `robots` meta tag support.
|
|
10
|
+
- Add richer optional schema fields for article-like page types.
|
|
11
|
+
- Add README common pattern examples.
|
|
12
|
+
- Add focused rendered metadata output coverage.
|
|
13
|
+
- Add soft validation helpers with `warnings` and `valid?`.
|
|
14
|
+
- Improve README and gem metadata positioning.
|
|
15
|
+
|
|
7
16
|
## 1.0.13 - 2026-05-06
|
|
8
17
|
|
|
9
18
|
- Add `PageStructuredData::PageTypes::Person` for reusable schema.org person values.
|
data/README.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
[](https://rubygems.org/gems/page_structured_data)
|
|
4
4
|
[](MIT-LICENSE)
|
|
5
5
|
|
|
6
|
-
PageStructuredData is a small Rails engine
|
|
6
|
+
PageStructuredData is a small Rails engine that keeps SEO metadata, social sharing tags, and JSON-LD structured data in one page object and one view partial.
|
|
7
|
+
|
|
8
|
+
Use it when a Rails app has metadata spread across layouts, helpers, presenters, and page-specific templates. PageStructuredData gives each page one explicit metadata object, then renders the `<title>`, meta tags, Open Graph tags, Twitter Card tags, breadcrumbs, and schema.org JSON-LD consistently.
|
|
7
9
|
|
|
8
10
|
It helps Rails applications render:
|
|
9
11
|
|
|
@@ -98,7 +100,8 @@ Set `@page_meta` in the controller or view before the layout renders:
|
|
|
98
100
|
description: "Welcome to my page",
|
|
99
101
|
image: image_url("social/home.png"),
|
|
100
102
|
canonical_url: home_url,
|
|
101
|
-
fallback_image: image_url("social/default.png")
|
|
103
|
+
fallback_image: image_url("social/default.png"),
|
|
104
|
+
robots: "index,follow"
|
|
102
105
|
)
|
|
103
106
|
```
|
|
104
107
|
|
|
@@ -265,6 +268,182 @@ website_page_type = PageStructuredData::PageTypes::WebSite.new(
|
|
|
265
268
|
)
|
|
266
269
|
```
|
|
267
270
|
|
|
271
|
+
## Common Patterns
|
|
272
|
+
|
|
273
|
+
### Article Page
|
|
274
|
+
|
|
275
|
+
Use `BlogPosting` or `NewsArticle` when a page represents editorial content:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
publisher = PageStructuredData::PageTypes::Organization.new(
|
|
279
|
+
name: "Example",
|
|
280
|
+
url: root_url
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
author = PageStructuredData::PageTypes::Person.new(
|
|
284
|
+
name: @article.author.name,
|
|
285
|
+
url: author_url(@article.author)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
article_page_type = PageStructuredData::PageTypes::BlogPosting.new(
|
|
289
|
+
headline: @article.title,
|
|
290
|
+
description: @article.summary,
|
|
291
|
+
article_body: @article.body.to_plain_text,
|
|
292
|
+
url: article_url(@article),
|
|
293
|
+
main_entity_of_page: article_url(@article),
|
|
294
|
+
publisher: publisher,
|
|
295
|
+
article_section: @article.category.name,
|
|
296
|
+
keywords: @article.tags.pluck(:name),
|
|
297
|
+
word_count: @article.word_count,
|
|
298
|
+
in_language: "en",
|
|
299
|
+
published_at: @article.published_at,
|
|
300
|
+
updated_at: @article.updated_at,
|
|
301
|
+
authors: [author],
|
|
302
|
+
image: url_for(@article.cover_image),
|
|
303
|
+
likes_count: @article.likes_count
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
@page_meta = PageStructuredData::Page.new(
|
|
307
|
+
title: @article.title,
|
|
308
|
+
description: @article.summary,
|
|
309
|
+
image: url_for(@article.cover_image),
|
|
310
|
+
canonical_url: article_url(@article),
|
|
311
|
+
breadcrumb: article_breadcrumbs,
|
|
312
|
+
page_type: article_page_type
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Homepage
|
|
317
|
+
|
|
318
|
+
Use both `Organization` and `WebSite` when the page represents the public home of a site or organization:
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
founder = PageStructuredData::PageTypes::Person.new(
|
|
322
|
+
name: "Jane Doe",
|
|
323
|
+
url: "https://example.com/jane"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
organization = PageStructuredData::PageTypes::Organization.new(
|
|
327
|
+
name: "Example",
|
|
328
|
+
url: root_url,
|
|
329
|
+
description: "Useful software from Example",
|
|
330
|
+
logo: image_url("logo.png"),
|
|
331
|
+
same_as: ["https://github.com/example"],
|
|
332
|
+
founder: founder
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
website = PageStructuredData::PageTypes::WebSite.new(
|
|
336
|
+
name: "Example",
|
|
337
|
+
url: root_url,
|
|
338
|
+
description: "Useful software from Example",
|
|
339
|
+
publisher: organization
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
@page_meta = PageStructuredData::Page.new(
|
|
343
|
+
title: "Example",
|
|
344
|
+
description: "Useful software from Example",
|
|
345
|
+
image: image_url("social/home.png"),
|
|
346
|
+
canonical_url: root_url,
|
|
347
|
+
page_types: [organization, website],
|
|
348
|
+
render_breadcrumb_json_ld: false
|
|
349
|
+
)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Forum Or Community Post
|
|
353
|
+
|
|
354
|
+
Use `DiscussionForumPosting` for public, user-authored, timestamped posts:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
post_author = PageStructuredData::PageTypes::Person.new(
|
|
358
|
+
name: @post.user.name,
|
|
359
|
+
url: user_url(@post.user)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
post_page_type = PageStructuredData::PageTypes::DiscussionForumPosting.new(
|
|
363
|
+
headline: @post.title,
|
|
364
|
+
text: @post.content_plaintext,
|
|
365
|
+
url: post_url(@post),
|
|
366
|
+
published_at: @post.created_at,
|
|
367
|
+
updated_at: @post.updated_at,
|
|
368
|
+
authors: [post_author],
|
|
369
|
+
comments_count: @post.comments_count
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
@page_meta = PageStructuredData::Page.new(
|
|
373
|
+
title: @post.title,
|
|
374
|
+
description: @post.excerpt,
|
|
375
|
+
canonical_url: post_url(@post),
|
|
376
|
+
page_type: post_page_type
|
|
377
|
+
)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Only pass engagement counts that are public and visible on the rendered page.
|
|
381
|
+
|
|
382
|
+
### Page-Local Site Name
|
|
383
|
+
|
|
384
|
+
Override or suppress the global app name for one page:
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
PageStructuredData.base_app_name = "Example"
|
|
388
|
+
|
|
389
|
+
PageStructuredData::Page.new(
|
|
390
|
+
title: "Docs",
|
|
391
|
+
base_app_name: "Developer Docs"
|
|
392
|
+
).page_title
|
|
393
|
+
# => "Docs - Developer Docs"
|
|
394
|
+
|
|
395
|
+
PageStructuredData::Page.new(
|
|
396
|
+
title: "Minimal",
|
|
397
|
+
base_app_name: ""
|
|
398
|
+
).page_title
|
|
399
|
+
# => "Minimal"
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Breadcrumb JSON-LD Control
|
|
403
|
+
|
|
404
|
+
Control generated breadcrumb JSON-LD per page:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
PageStructuredData::Page.new(
|
|
408
|
+
title: "Landing Page",
|
|
409
|
+
render_breadcrumb_json_ld: false
|
|
410
|
+
)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
When the global default is disabled, a page can still opt in:
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
PageStructuredData.render_default_breadcrumb_json_ld = false
|
|
417
|
+
|
|
418
|
+
PageStructuredData::Page.new(
|
|
419
|
+
title: "Standalone Page",
|
|
420
|
+
render_breadcrumb_json_ld: true
|
|
421
|
+
)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Paginated Archive
|
|
425
|
+
|
|
426
|
+
Use `robots` for archive, search, filtered, or paginated pages that should be crawlable but not indexed:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
@page_meta = PageStructuredData::Page.new(
|
|
430
|
+
title: "Articles",
|
|
431
|
+
extra_title: "Page #{@pagy.page}",
|
|
432
|
+
description: "Browse articles from Example",
|
|
433
|
+
canonical_url: articles_url(page: @pagy.page),
|
|
434
|
+
robots: "noindex,follow"
|
|
435
|
+
)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`robots` can also be passed as an array:
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
PageStructuredData::Page.new(
|
|
442
|
+
title: "Articles",
|
|
443
|
+
robots: ["noindex", "follow"]
|
|
444
|
+
)
|
|
445
|
+
```
|
|
446
|
+
|
|
268
447
|
## API Reference
|
|
269
448
|
|
|
270
449
|
### `PageStructuredData::Page`
|
|
@@ -281,18 +460,42 @@ PageStructuredData::Page.new(
|
|
|
281
460
|
canonical_url: nil,
|
|
282
461
|
fallback_image: nil,
|
|
283
462
|
base_app_name: nil,
|
|
284
|
-
render_breadcrumb_json_ld: nil
|
|
463
|
+
render_breadcrumb_json_ld: nil,
|
|
464
|
+
robots: nil
|
|
285
465
|
)
|
|
286
466
|
```
|
|
287
467
|
|
|
288
468
|
`base_app_name` overrides `PageStructuredData.base_app_name` for one page. Pass an empty string to suppress the global app name for a specific page.
|
|
289
469
|
`render_breadcrumb_json_ld` can be set to `true` or `false` for one page. Leave it as `nil` to use the global `PageStructuredData.render_default_breadcrumb_json_ld` behavior for generated default breadcrumbs. Explicit breadcrumb objects still render when the global default is disabled unless the page sets `render_breadcrumb_json_ld: false`.
|
|
470
|
+
`robots` can be a string or array and renders a `<meta name="robots">` tag when present.
|
|
290
471
|
|
|
291
472
|
Important methods:
|
|
292
473
|
|
|
293
474
|
- `page_title`: returns the composed page title.
|
|
294
475
|
- `json_lds`: returns the JSON-LD script tags for breadcrumbs and page type data.
|
|
295
476
|
- `resolved_image`: returns `image` or `fallback_image`.
|
|
477
|
+
- `robots_content`: returns the rendered robots directives.
|
|
478
|
+
- `warnings`: returns soft validation warnings for the page and page types.
|
|
479
|
+
- `valid?`: returns `true` when `warnings` is empty.
|
|
480
|
+
|
|
481
|
+
### Soft Validation
|
|
482
|
+
|
|
483
|
+
Page objects and page type objects expose non-blocking validation helpers:
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
page_type = PageStructuredData::PageTypes::Organization.new(
|
|
487
|
+
name: nil,
|
|
488
|
+
url: nil
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
page_type.warnings
|
|
492
|
+
# => ["name is required", "url is required"]
|
|
493
|
+
|
|
494
|
+
page_type.valid?
|
|
495
|
+
# => false
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`warnings` does not stop rendering. It is intended for tests, previews, and development checks where you want to catch incomplete structured data before publishing a page.
|
|
296
499
|
|
|
297
500
|
### `PageStructuredData::Breadcrumbs`
|
|
298
501
|
|
|
@@ -320,9 +523,16 @@ PageStructuredData::PageTypes::BlogPosting.new(
|
|
|
320
523
|
images: [],
|
|
321
524
|
authors: [],
|
|
322
525
|
image: nil,
|
|
526
|
+
description: nil,
|
|
323
527
|
article_body: nil,
|
|
324
528
|
text: nil,
|
|
325
529
|
url: nil,
|
|
530
|
+
main_entity_of_page: nil,
|
|
531
|
+
publisher: nil,
|
|
532
|
+
article_section: nil,
|
|
533
|
+
keywords: nil,
|
|
534
|
+
word_count: nil,
|
|
535
|
+
in_language: nil,
|
|
326
536
|
interaction_statistics: [],
|
|
327
537
|
likes_count: nil,
|
|
328
538
|
comments_count: nil,
|
|
@@ -338,9 +548,16 @@ PageStructuredData::PageTypes::NewsArticle.new(
|
|
|
338
548
|
images: [],
|
|
339
549
|
authors: [],
|
|
340
550
|
image: nil,
|
|
551
|
+
description: nil,
|
|
341
552
|
article_body: nil,
|
|
342
553
|
text: nil,
|
|
343
554
|
url: nil,
|
|
555
|
+
main_entity_of_page: nil,
|
|
556
|
+
publisher: nil,
|
|
557
|
+
article_section: nil,
|
|
558
|
+
keywords: nil,
|
|
559
|
+
word_count: nil,
|
|
560
|
+
in_language: nil,
|
|
344
561
|
interaction_statistics: [],
|
|
345
562
|
likes_count: nil,
|
|
346
563
|
comments_count: nil,
|
|
@@ -351,6 +568,9 @@ PageStructuredData::PageTypes::NewsArticle.new(
|
|
|
351
568
|
`authors` can be an array of hashes, `PageStructuredData::PageTypes::Person` objects, or other objects that respond to `to_h`.
|
|
352
569
|
`image` is a convenience option for one image URL. Use `images` when passing multiple image URLs.
|
|
353
570
|
`text` is an alias for `article_body`.
|
|
571
|
+
`main_entity_of_page` can be a URL, hash, or object that responds to `to_h`.
|
|
572
|
+
`publisher` can be a hash or another page type that responds to `to_h`, such as `PageStructuredData::PageTypes::Organization`.
|
|
573
|
+
`keywords` can be a string or an array.
|
|
354
574
|
`interaction_statistics` should be an array of `PageStructuredData::PageTypes::InteractionStatistic` objects or schema-compatible hashes.
|
|
355
575
|
|
|
356
576
|
Important methods:
|
|
@@ -366,9 +586,16 @@ PageStructuredData::PageTypes::DiscussionForumPosting.new(
|
|
|
366
586
|
images: [],
|
|
367
587
|
authors: [],
|
|
368
588
|
image: nil,
|
|
589
|
+
description: nil,
|
|
369
590
|
article_body: nil,
|
|
370
591
|
text: nil,
|
|
371
592
|
url: nil,
|
|
593
|
+
main_entity_of_page: nil,
|
|
594
|
+
publisher: nil,
|
|
595
|
+
article_section: nil,
|
|
596
|
+
keywords: nil,
|
|
597
|
+
word_count: nil,
|
|
598
|
+
in_language: nil,
|
|
372
599
|
interaction_statistics: [],
|
|
373
600
|
likes_count: nil,
|
|
374
601
|
comments_count: nil,
|
|
@@ -4,11 +4,11 @@ module PageStructuredData
|
|
|
4
4
|
# Basic page metadata for any page
|
|
5
5
|
class Page
|
|
6
6
|
attr_reader :title, :description, :image, :extra_title, :breadcrumb, :page_type, :page_types, :canonical_url,
|
|
7
|
-
:fallback_image, :base_app_name, :render_breadcrumb_json_ld
|
|
7
|
+
:fallback_image, :base_app_name, :render_breadcrumb_json_ld, :robots
|
|
8
8
|
|
|
9
9
|
def initialize(title:, description: nil, image: nil, # rubocop:disable Metrics/ParameterLists
|
|
10
10
|
extra_title: '', breadcrumb: nil, page_type: nil, page_types: nil, canonical_url: nil,
|
|
11
|
-
fallback_image: nil, base_app_name: nil, render_breadcrumb_json_ld: nil)
|
|
11
|
+
fallback_image: nil, base_app_name: nil, render_breadcrumb_json_ld: nil, robots: nil)
|
|
12
12
|
@title = title
|
|
13
13
|
@description = description
|
|
14
14
|
@image = image
|
|
@@ -20,6 +20,7 @@ module PageStructuredData
|
|
|
20
20
|
@fallback_image = fallback_image
|
|
21
21
|
@base_app_name = base_app_name
|
|
22
22
|
@render_breadcrumb_json_ld = render_breadcrumb_json_ld
|
|
23
|
+
@robots = robots
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def title_with_hierarchies
|
|
@@ -47,12 +48,36 @@ module PageStructuredData
|
|
|
47
48
|
image || fallback_image
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
def robots_content
|
|
52
|
+
Array(robots).compact.join(',')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def warnings
|
|
56
|
+
page_warnings + page_type_warnings
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def valid?
|
|
60
|
+
warnings.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
50
63
|
private
|
|
51
64
|
|
|
52
65
|
def resolved_page_types
|
|
53
66
|
Array.wrap(page_types.presence || page_type).compact
|
|
54
67
|
end
|
|
55
68
|
|
|
69
|
+
def page_warnings
|
|
70
|
+
title.present? ? [] : ['title is required']
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def page_type_warnings
|
|
74
|
+
resolved_page_types.each_with_index.flat_map do |resolved_page_type, index|
|
|
75
|
+
next [] unless resolved_page_type.respond_to?(:warnings)
|
|
76
|
+
|
|
77
|
+
resolved_page_type.warnings.map { |warning| "page type #{index + 1}: #{warning}" }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
56
81
|
def breadcrumb_json_ld
|
|
57
82
|
return if render_breadcrumb_json_ld == false
|
|
58
83
|
return breadcrumb.json_ld(current_page_title: title) if breadcrumb.present?
|
|
@@ -6,18 +6,28 @@ module PageStructuredData
|
|
|
6
6
|
class Article
|
|
7
7
|
include SchemaNode
|
|
8
8
|
|
|
9
|
-
attr_reader :headline, :images, :published_at, :updated_at, :authors, :article_body, :url,
|
|
9
|
+
attr_reader :headline, :images, :published_at, :updated_at, :authors, :description, :article_body, :url,
|
|
10
|
+
:main_entity_of_page, :publisher, :article_section, :keywords, :word_count, :in_language,
|
|
10
11
|
:interaction_statistics, :likes_count, :comments_count, :shares_count
|
|
11
12
|
|
|
12
13
|
def initialize(headline:, published_at:, updated_at:, images: [], authors: [], image: nil, article_body: nil, text: nil,
|
|
13
|
-
|
|
14
|
+
description: nil, url: nil, main_entity_of_page: nil, publisher: nil, article_section: nil,
|
|
15
|
+
keywords: nil, word_count: nil, in_language: nil, interaction_statistics: [], likes_count: nil,
|
|
16
|
+
comments_count: nil, shares_count: nil)
|
|
14
17
|
@headline = headline
|
|
15
18
|
@images = image.present? ? Array(image) : Array(images)
|
|
16
19
|
@published_at = published_at
|
|
17
20
|
@updated_at = updated_at
|
|
18
21
|
@authors = Array(authors)
|
|
22
|
+
@description = description
|
|
19
23
|
@article_body = article_body || text
|
|
20
24
|
@url = url
|
|
25
|
+
@main_entity_of_page = main_entity_of_page
|
|
26
|
+
@publisher = publisher
|
|
27
|
+
@article_section = article_section
|
|
28
|
+
@keywords = keywords
|
|
29
|
+
@word_count = word_count
|
|
30
|
+
@in_language = in_language
|
|
21
31
|
@interaction_statistics = Array(interaction_statistics)
|
|
22
32
|
@likes_count = likes_count
|
|
23
33
|
@comments_count = comments_count
|
|
@@ -35,8 +45,15 @@ module PageStructuredData
|
|
|
35
45
|
author: authors.map { |author| author_to_h(author) },
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
node[:description] = description if description.present?
|
|
38
49
|
node[:articleBody] = article_body if article_body.present?
|
|
39
50
|
node[:url] = url if url.present?
|
|
51
|
+
node[:mainEntityOfPage] = object_to_h(main_entity_of_page) if main_entity_of_page.present?
|
|
52
|
+
node[:publisher] = object_to_h(publisher) if publisher.present?
|
|
53
|
+
node[:articleSection] = article_section if article_section.present?
|
|
54
|
+
node[:keywords] = keywords if keywords.present?
|
|
55
|
+
node[:wordCount] = word_count if word_count.present?
|
|
56
|
+
node[:inLanguage] = object_to_h(in_language) if in_language.present?
|
|
40
57
|
node[:interactionStatistic] = interaction_statistics_to_h if interaction_statistics_to_h.any?
|
|
41
58
|
|
|
42
59
|
node
|
|
@@ -50,6 +67,14 @@ module PageStructuredData
|
|
|
50
67
|
)
|
|
51
68
|
end
|
|
52
69
|
|
|
70
|
+
def warnings
|
|
71
|
+
required_attribute_warnings(
|
|
72
|
+
headline: headline,
|
|
73
|
+
published_at: published_at,
|
|
74
|
+
updated_at: updated_at
|
|
75
|
+
) + author_warnings + publisher_warnings + interaction_statistic_warnings
|
|
76
|
+
end
|
|
77
|
+
|
|
53
78
|
private
|
|
54
79
|
|
|
55
80
|
def schema_type
|
|
@@ -68,6 +93,25 @@ module PageStructuredData
|
|
|
68
93
|
)
|
|
69
94
|
end
|
|
70
95
|
|
|
96
|
+
def author_warnings
|
|
97
|
+
authors.each_with_index.flat_map do |author, index|
|
|
98
|
+
if author.respond_to?(:warnings)
|
|
99
|
+
author.warnings.map { |warning| "author #{index + 1}: #{warning}" }
|
|
100
|
+
else
|
|
101
|
+
author_hash = object_to_h(author) || {}
|
|
102
|
+
required_attribute_warnings(name: author_hash[:name] || author_hash['name']).map do |warning|
|
|
103
|
+
"author #{index + 1}: #{warning}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def publisher_warnings
|
|
110
|
+
return [] unless publisher.respond_to?(:warnings)
|
|
111
|
+
|
|
112
|
+
publisher.warnings.map { |warning| "publisher: #{warning}" }
|
|
113
|
+
end
|
|
114
|
+
|
|
71
115
|
def interaction_statistics_to_h
|
|
72
116
|
@interaction_statistics_to_h ||= all_interaction_statistics.map do |interaction_statistic|
|
|
73
117
|
if interaction_statistic.respond_to?(:to_h)
|
|
@@ -95,6 +139,14 @@ module PageStructuredData
|
|
|
95
139
|
|
|
96
140
|
InteractionStatistic.new(interaction_type: interaction_type, user_interaction_count: count)
|
|
97
141
|
end
|
|
142
|
+
|
|
143
|
+
def interaction_statistic_warnings
|
|
144
|
+
all_interaction_statistics.each_with_index.flat_map do |interaction_statistic, index|
|
|
145
|
+
next [] unless interaction_statistic.respond_to?(:warnings)
|
|
146
|
+
|
|
147
|
+
interaction_statistic.warnings.map { |warning| "interaction statistic #{index + 1}: #{warning}" }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
98
150
|
end
|
|
99
151
|
end
|
|
100
152
|
end
|
|
@@ -40,8 +40,21 @@ module PageStructuredData
|
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def warnings
|
|
44
|
+
required_attribute_warnings(name: name, url: url) + nested_warnings
|
|
45
|
+
end
|
|
46
|
+
|
|
43
47
|
private
|
|
44
48
|
|
|
49
|
+
def nested_warnings
|
|
50
|
+
[founder, parent_organization].each_with_index.flat_map do |node, index|
|
|
51
|
+
next [] unless node.respond_to?(:warnings)
|
|
52
|
+
|
|
53
|
+
prefix = index.zero? ? 'founder' : 'parent organization'
|
|
54
|
+
node.warnings.map { |warning| "#{prefix}: #{warning}" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
45
58
|
def parent_organization_to_h
|
|
46
59
|
return object_to_h(parent_organization) if parent_organization.respond_to?(:to_h) && !parent_organization.is_a?(Hash)
|
|
47
60
|
return unless parent_organization.present?
|
|
@@ -4,6 +4,14 @@ module PageStructuredData
|
|
|
4
4
|
module PageTypes
|
|
5
5
|
# Shared helpers for schema.org hash values.
|
|
6
6
|
module SchemaNode
|
|
7
|
+
def warnings
|
|
8
|
+
[]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def valid?
|
|
12
|
+
warnings.empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
private
|
|
8
16
|
|
|
9
17
|
def compact_node(node)
|
|
@@ -23,6 +31,12 @@ module PageStructuredData
|
|
|
23
31
|
def blank_schema_value?(value)
|
|
24
32
|
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
25
33
|
end
|
|
34
|
+
|
|
35
|
+
def required_attribute_warnings(attributes)
|
|
36
|
+
attributes.each_with_object([]) do |(name, value), warnings|
|
|
37
|
+
warnings << "#{name} is required" if blank_schema_value?(value)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
26
40
|
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
@@ -36,6 +36,17 @@ module PageStructuredData
|
|
|
36
36
|
)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def warnings
|
|
40
|
+
required_attribute_warnings(name: name, url: url) + nested_warnings
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def nested_warnings
|
|
46
|
+
return [] unless publisher.respond_to?(:warnings)
|
|
47
|
+
|
|
48
|
+
publisher.warnings.map { |warning| "publisher: #{warning}" }
|
|
49
|
+
end
|
|
39
50
|
end
|
|
40
51
|
end
|
|
41
52
|
end
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
<% description = page&.description %>
|
|
5
5
|
<% image = page&.resolved_image || default_image_url || nil %>
|
|
6
6
|
<% canonical_url = page&.canonical_url %>
|
|
7
|
+
<% robots_content = page&.robots_content %>
|
|
7
8
|
|
|
8
9
|
<title><%= title %></title>
|
|
9
10
|
<% if canonical_url.present? %>
|
|
10
11
|
<link rel="canonical" href="<%= canonical_url %>">
|
|
11
12
|
<% end %>
|
|
13
|
+
<% if robots_content.present? %>
|
|
14
|
+
<meta name="robots" content="<%= robots_content %>">
|
|
15
|
+
<% end %>
|
|
12
16
|
|
|
13
17
|
<meta name="title" content="<%= title %>">
|
|
14
18
|
<% if description.present? %>
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: page_structured_data
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jey Geethan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05
|
|
11
|
+
date: 2026-07-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -30,10 +30,11 @@ dependencies:
|
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '9.0'
|
|
33
|
-
description: PageStructuredData
|
|
34
|
-
|
|
35
|
-
tags,
|
|
36
|
-
and WebSite JSON-LD, and public interaction
|
|
33
|
+
description: PageStructuredData keeps Rails SEO metadata, social sharing tags, and
|
|
34
|
+
schema.org JSON-LD in one page object and one view partial. It renders page titles,
|
|
35
|
+
basic meta tags, Open Graph tags, Twitter Card tags, breadcrumb JSON-LD, article
|
|
36
|
+
and forum post JSON-LD, Person, Organization, and WebSite JSON-LD, and public interaction
|
|
37
|
+
statistics.
|
|
37
38
|
email:
|
|
38
39
|
- opensource@rocketapex.com
|
|
39
40
|
executables: []
|
|
@@ -91,5 +92,5 @@ requirements: []
|
|
|
91
92
|
rubygems_version: 3.3.7
|
|
92
93
|
signing_key:
|
|
93
94
|
specification_version: 4
|
|
94
|
-
summary:
|
|
95
|
+
summary: One Rails page object for SEO, social, and JSON-LD metadata
|
|
95
96
|
test_files: []
|