page_structured_data 1.0.12 → 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 +16 -0
- data/README.md +262 -4
- data/app/src/page_structured_data/page.rb +42 -6
- data/app/src/page_structured_data/page_types/article.rb +69 -9
- data/app/src/page_structured_data/page_types/interaction_statistic.rb +11 -11
- data/app/src/page_structured_data/page_types/organization.rb +33 -23
- data/app/src/page_structured_data/page_types/person.rb +33 -0
- data/app/src/page_structured_data/page_types/schema_node.rb +42 -0
- data/app/src/page_structured_data/page_types/web_site.rb +14 -11
- data/app/views/page_structured_data/_meta_tags.html.erb +16 -0
- data/lib/page_structured_data/version.rb +1 -1
- data/lib/page_structured_data.rb +2 -0
- metadata +10 -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,22 @@ 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
|
+
|
|
16
|
+
## 1.0.13 - 2026-05-06
|
|
17
|
+
|
|
18
|
+
- Add `PageStructuredData::PageTypes::Person` for reusable schema.org person values.
|
|
19
|
+
- Accept richer article author values and omit blank nested schema fields.
|
|
20
|
+
- Add page-level `base_app_name` and `render_breadcrumb_json_ld` options.
|
|
21
|
+
- Omit blank description and image meta tags.
|
|
22
|
+
|
|
7
23
|
## 1.0.12 - 2026-05-06
|
|
8
24
|
|
|
9
25
|
- Add reusable interaction statistics for article-like page types.
|
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
|
|
|
@@ -16,6 +18,7 @@ It helps Rails applications render:
|
|
|
16
18
|
- Article structured data for `BlogPosting` and `NewsArticle`
|
|
17
19
|
- Discussion forum post structured data
|
|
18
20
|
- Interaction statistics for public engagement counts
|
|
21
|
+
- Reusable Person structured data
|
|
19
22
|
- Organization and WebSite structured data
|
|
20
23
|
|
|
21
24
|
## Requirements
|
|
@@ -97,7 +100,8 @@ Set `@page_meta` in the controller or view before the layout renders:
|
|
|
97
100
|
description: "Welcome to my page",
|
|
98
101
|
image: image_url("social/home.png"),
|
|
99
102
|
canonical_url: home_url,
|
|
100
|
-
fallback_image: image_url("social/default.png")
|
|
103
|
+
fallback_image: image_url("social/default.png"),
|
|
104
|
+
robots: "index,follow"
|
|
101
105
|
)
|
|
102
106
|
```
|
|
103
107
|
|
|
@@ -144,6 +148,7 @@ PageStructuredData includes page types for:
|
|
|
144
148
|
- [`BlogPosting`](https://schema.org/BlogPosting)
|
|
145
149
|
- [`NewsArticle`](https://schema.org/NewsArticle)
|
|
146
150
|
- [`DiscussionForumPosting`](https://schema.org/DiscussionForumPosting)
|
|
151
|
+
- [`Person`](https://schema.org/Person)
|
|
147
152
|
- [`Organization`](https://schema.org/Organization)
|
|
148
153
|
- [`WebSite`](https://schema.org/WebSite)
|
|
149
154
|
|
|
@@ -263,6 +268,182 @@ website_page_type = PageStructuredData::PageTypes::WebSite.new(
|
|
|
263
268
|
)
|
|
264
269
|
```
|
|
265
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
|
+
|
|
266
447
|
## API Reference
|
|
267
448
|
|
|
268
449
|
### `PageStructuredData::Page`
|
|
@@ -277,15 +458,44 @@ PageStructuredData::Page.new(
|
|
|
277
458
|
page_type: nil,
|
|
278
459
|
page_types: nil,
|
|
279
460
|
canonical_url: nil,
|
|
280
|
-
fallback_image: nil
|
|
461
|
+
fallback_image: nil,
|
|
462
|
+
base_app_name: nil,
|
|
463
|
+
render_breadcrumb_json_ld: nil,
|
|
464
|
+
robots: nil
|
|
281
465
|
)
|
|
282
466
|
```
|
|
283
467
|
|
|
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.
|
|
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.
|
|
471
|
+
|
|
284
472
|
Important methods:
|
|
285
473
|
|
|
286
474
|
- `page_title`: returns the composed page title.
|
|
287
475
|
- `json_lds`: returns the JSON-LD script tags for breadcrumbs and page type data.
|
|
288
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.
|
|
289
499
|
|
|
290
500
|
### `PageStructuredData::Breadcrumbs`
|
|
291
501
|
|
|
@@ -313,9 +523,16 @@ PageStructuredData::PageTypes::BlogPosting.new(
|
|
|
313
523
|
images: [],
|
|
314
524
|
authors: [],
|
|
315
525
|
image: nil,
|
|
526
|
+
description: nil,
|
|
316
527
|
article_body: nil,
|
|
317
528
|
text: nil,
|
|
318
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,
|
|
319
536
|
interaction_statistics: [],
|
|
320
537
|
likes_count: nil,
|
|
321
538
|
comments_count: nil,
|
|
@@ -331,9 +548,16 @@ PageStructuredData::PageTypes::NewsArticle.new(
|
|
|
331
548
|
images: [],
|
|
332
549
|
authors: [],
|
|
333
550
|
image: nil,
|
|
551
|
+
description: nil,
|
|
334
552
|
article_body: nil,
|
|
335
553
|
text: nil,
|
|
336
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,
|
|
337
561
|
interaction_statistics: [],
|
|
338
562
|
likes_count: nil,
|
|
339
563
|
comments_count: nil,
|
|
@@ -341,9 +565,12 @@ PageStructuredData::PageTypes::NewsArticle.new(
|
|
|
341
565
|
)
|
|
342
566
|
```
|
|
343
567
|
|
|
344
|
-
`authors`
|
|
568
|
+
`authors` can be an array of hashes, `PageStructuredData::PageTypes::Person` objects, or other objects that respond to `to_h`.
|
|
345
569
|
`image` is a convenience option for one image URL. Use `images` when passing multiple image URLs.
|
|
346
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.
|
|
347
574
|
`interaction_statistics` should be an array of `PageStructuredData::PageTypes::InteractionStatistic` objects or schema-compatible hashes.
|
|
348
575
|
|
|
349
576
|
Important methods:
|
|
@@ -359,9 +586,16 @@ PageStructuredData::PageTypes::DiscussionForumPosting.new(
|
|
|
359
586
|
images: [],
|
|
360
587
|
authors: [],
|
|
361
588
|
image: nil,
|
|
589
|
+
description: nil,
|
|
362
590
|
article_body: nil,
|
|
363
591
|
text: nil,
|
|
364
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,
|
|
365
599
|
interaction_statistics: [],
|
|
366
600
|
likes_count: nil,
|
|
367
601
|
comments_count: nil,
|
|
@@ -393,6 +627,30 @@ Important methods:
|
|
|
393
627
|
|
|
394
628
|
- `to_h`: returns a structured hash for `InteractionCounter` JSON-LD.
|
|
395
629
|
|
|
630
|
+
### Person
|
|
631
|
+
|
|
632
|
+
```ruby
|
|
633
|
+
PageStructuredData::PageTypes::Person.new(
|
|
634
|
+
name:,
|
|
635
|
+
url: nil,
|
|
636
|
+
image: nil,
|
|
637
|
+
same_as: []
|
|
638
|
+
)
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Use `Person` for article authors, organization founders, and other schema.org person values:
|
|
642
|
+
|
|
643
|
+
```ruby
|
|
644
|
+
author = PageStructuredData::PageTypes::Person.new(
|
|
645
|
+
name: "Jane Doe",
|
|
646
|
+
url: "https://example.com/jane"
|
|
647
|
+
)
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Important methods:
|
|
651
|
+
|
|
652
|
+
- `to_h`: returns a compact structured hash for `Person` JSON-LD.
|
|
653
|
+
|
|
396
654
|
### Organization Page Type
|
|
397
655
|
|
|
398
656
|
```ruby
|
|
@@ -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
|
|
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)
|
|
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
|
|
@@ -18,6 +18,9 @@ module PageStructuredData
|
|
|
18
18
|
@page_types = page_types
|
|
19
19
|
@canonical_url = canonical_url
|
|
20
20
|
@fallback_image = fallback_image
|
|
21
|
+
@base_app_name = base_app_name
|
|
22
|
+
@render_breadcrumb_json_ld = render_breadcrumb_json_ld
|
|
23
|
+
@robots = robots
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def title_with_hierarchies
|
|
@@ -28,8 +31,8 @@ module PageStructuredData
|
|
|
28
31
|
|
|
29
32
|
def page_title
|
|
30
33
|
result = title_with_hierarchies.join(separator)
|
|
31
|
-
if
|
|
32
|
-
result += separator +
|
|
34
|
+
if resolved_base_app_name.present?
|
|
35
|
+
result += separator + resolved_base_app_name
|
|
33
36
|
end
|
|
34
37
|
result
|
|
35
38
|
end
|
|
@@ -45,20 +48,53 @@ module PageStructuredData
|
|
|
45
48
|
image || fallback_image
|
|
46
49
|
end
|
|
47
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
|
+
|
|
48
63
|
private
|
|
49
64
|
|
|
50
65
|
def resolved_page_types
|
|
51
66
|
Array.wrap(page_types.presence || page_type).compact
|
|
52
67
|
end
|
|
53
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
|
+
|
|
54
81
|
def breadcrumb_json_ld
|
|
82
|
+
return if render_breadcrumb_json_ld == false
|
|
55
83
|
return breadcrumb.json_ld(current_page_title: title) if breadcrumb.present?
|
|
56
|
-
return unless
|
|
84
|
+
return unless render_breadcrumb_json_ld?
|
|
57
85
|
|
|
58
86
|
Breadcrumbs.new.json_ld(current_page_title: title)
|
|
59
87
|
end
|
|
60
88
|
|
|
61
|
-
def
|
|
89
|
+
def render_breadcrumb_json_ld?
|
|
90
|
+
return render_breadcrumb_json_ld unless render_breadcrumb_json_ld.nil?
|
|
91
|
+
|
|
92
|
+
PageStructuredData.render_default_breadcrumb_json_ld
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def resolved_base_app_name
|
|
96
|
+
return base_app_name unless base_app_name.nil?
|
|
97
|
+
|
|
62
98
|
PageStructuredData.base_app_name
|
|
63
99
|
end
|
|
64
100
|
|
|
@@ -4,18 +4,30 @@ module PageStructuredData
|
|
|
4
4
|
module PageTypes
|
|
5
5
|
# Shared structured data for schema.org article-like page types.
|
|
6
6
|
class Article
|
|
7
|
-
|
|
7
|
+
include SchemaNode
|
|
8
|
+
|
|
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,
|
|
8
11
|
:interaction_statistics, :likes_count, :comments_count, :shares_count
|
|
9
12
|
|
|
10
13
|
def initialize(headline:, published_at:, updated_at:, images: [], authors: [], image: nil, article_body: nil, text: nil,
|
|
11
|
-
|
|
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)
|
|
12
17
|
@headline = headline
|
|
13
18
|
@images = image.present? ? Array(image) : Array(images)
|
|
14
19
|
@published_at = published_at
|
|
15
20
|
@updated_at = updated_at
|
|
16
21
|
@authors = Array(authors)
|
|
22
|
+
@description = description
|
|
17
23
|
@article_body = article_body || text
|
|
18
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
|
|
19
31
|
@interaction_statistics = Array(interaction_statistics)
|
|
20
32
|
@likes_count = likes_count
|
|
21
33
|
@comments_count = comments_count
|
|
@@ -30,17 +42,18 @@ module PageStructuredData
|
|
|
30
42
|
image: images,
|
|
31
43
|
datePublished: published_at,
|
|
32
44
|
dateModified: updated_at,
|
|
33
|
-
author: authors.map
|
|
34
|
-
{
|
|
35
|
-
'@type': 'Person',
|
|
36
|
-
name: author[:name],
|
|
37
|
-
url: author[:url],
|
|
38
|
-
}
|
|
39
|
-
end,
|
|
45
|
+
author: authors.map { |author| author_to_h(author) },
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
node[:description] = description if description.present?
|
|
42
49
|
node[:articleBody] = article_body if article_body.present?
|
|
43
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?
|
|
44
57
|
node[:interactionStatistic] = interaction_statistics_to_h if interaction_statistics_to_h.any?
|
|
45
58
|
|
|
46
59
|
node
|
|
@@ -54,12 +67,51 @@ module PageStructuredData
|
|
|
54
67
|
)
|
|
55
68
|
end
|
|
56
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
|
+
|
|
57
78
|
private
|
|
58
79
|
|
|
59
80
|
def schema_type
|
|
60
81
|
raise NotImplementedError, "#{self.class.name} must define #schema_type"
|
|
61
82
|
end
|
|
62
83
|
|
|
84
|
+
def author_to_h(author)
|
|
85
|
+
return object_to_h(author) if author.respond_to?(:to_h) && !author.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
compact_node(
|
|
88
|
+
'@type': 'Person',
|
|
89
|
+
name: author[:name] || author['name'],
|
|
90
|
+
url: author[:url] || author['url'],
|
|
91
|
+
image: author[:image] || author['image'],
|
|
92
|
+
sameAs: author[:same_as] || author[:sameAs] || author['same_as'] || author['sameAs']
|
|
93
|
+
)
|
|
94
|
+
end
|
|
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
|
+
|
|
63
115
|
def interaction_statistics_to_h
|
|
64
116
|
@interaction_statistics_to_h ||= all_interaction_statistics.map do |interaction_statistic|
|
|
65
117
|
if interaction_statistic.respond_to?(:to_h)
|
|
@@ -87,6 +139,14 @@ module PageStructuredData
|
|
|
87
139
|
|
|
88
140
|
InteractionStatistic.new(interaction_type: interaction_type, user_interaction_count: count)
|
|
89
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
|
|
90
150
|
end
|
|
91
151
|
end
|
|
92
152
|
end
|
|
@@ -4,6 +4,8 @@ module PageStructuredData
|
|
|
4
4
|
module PageTypes
|
|
5
5
|
# schema.org InteractionCounter structured data.
|
|
6
6
|
class InteractionStatistic
|
|
7
|
+
include SchemaNode
|
|
8
|
+
|
|
7
9
|
ACTION_TYPES = {
|
|
8
10
|
like: 'LikeAction',
|
|
9
11
|
likes: 'LikeAction',
|
|
@@ -34,15 +36,19 @@ module PageStructuredData
|
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def to_h
|
|
37
|
-
|
|
39
|
+
compact_node(
|
|
38
40
|
'@type': 'InteractionCounter',
|
|
39
41
|
interactionType: interaction_type_to_h,
|
|
40
42
|
userInteractionCount: user_interaction_count,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
interactionService: object_to_h(interaction_service)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
def warnings
|
|
48
|
+
required_attribute_warnings(
|
|
49
|
+
interaction_type: interaction_type,
|
|
50
|
+
user_interaction_count: user_interaction_count
|
|
51
|
+
)
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
private
|
|
@@ -54,12 +60,6 @@ module PageStructuredData
|
|
|
54
60
|
|
|
55
61
|
{ '@type': type }
|
|
56
62
|
end
|
|
57
|
-
|
|
58
|
-
def object_to_h(object)
|
|
59
|
-
return object.to_h if object.respond_to?(:to_h)
|
|
60
|
-
|
|
61
|
-
object
|
|
62
|
-
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -4,6 +4,8 @@ module PageStructuredData
|
|
|
4
4
|
module PageTypes
|
|
5
5
|
# Organization structured data for a page
|
|
6
6
|
class Organization
|
|
7
|
+
include SchemaNode
|
|
8
|
+
|
|
7
9
|
attr_reader :name, :url, :description, :logo, :same_as, :parent_organization, :founder
|
|
8
10
|
|
|
9
11
|
def initialize(name:, url:, description: nil, logo: nil, same_as: [], parent_organization: nil, founder: nil)
|
|
@@ -11,33 +13,23 @@ module PageStructuredData
|
|
|
11
13
|
@url = url
|
|
12
14
|
@description = description
|
|
13
15
|
@logo = logo
|
|
14
|
-
@same_as = same_as
|
|
16
|
+
@same_as = Array(same_as)
|
|
15
17
|
@parent_organization = parent_organization
|
|
16
18
|
@founder = founder
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def to_h # rubocop:disable Metrics/MethodLength
|
|
20
|
-
|
|
22
|
+
compact_node(
|
|
21
23
|
'@context': 'https://schema.org',
|
|
22
24
|
'@type': 'Organization',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if parent_organization.present?
|
|
33
|
-
node[:parentOrganization] = {
|
|
34
|
-
'@type': 'Organization',
|
|
35
|
-
name: parent_organization[:name],
|
|
36
|
-
url: parent_organization[:url],
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
node
|
|
25
|
+
name: name,
|
|
26
|
+
url: url,
|
|
27
|
+
description: description,
|
|
28
|
+
logo: logo,
|
|
29
|
+
sameAs: same_as,
|
|
30
|
+
founder: object_to_h(founder),
|
|
31
|
+
parentOrganization: parent_organization_to_h
|
|
32
|
+
)
|
|
41
33
|
end
|
|
42
34
|
|
|
43
35
|
def json_ld
|
|
@@ -48,12 +40,30 @@ module PageStructuredData
|
|
|
48
40
|
)
|
|
49
41
|
end
|
|
50
42
|
|
|
43
|
+
def warnings
|
|
44
|
+
required_attribute_warnings(name: name, url: url) + nested_warnings
|
|
45
|
+
end
|
|
46
|
+
|
|
51
47
|
private
|
|
52
48
|
|
|
53
|
-
def
|
|
54
|
-
|
|
49
|
+
def nested_warnings
|
|
50
|
+
[founder, parent_organization].each_with_index.flat_map do |node, index|
|
|
51
|
+
next [] unless node.respond_to?(:warnings)
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
prefix = index.zero? ? 'founder' : 'parent organization'
|
|
54
|
+
node.warnings.map { |warning| "#{prefix}: #{warning}" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parent_organization_to_h
|
|
59
|
+
return object_to_h(parent_organization) if parent_organization.respond_to?(:to_h) && !parent_organization.is_a?(Hash)
|
|
60
|
+
return unless parent_organization.present?
|
|
61
|
+
|
|
62
|
+
compact_node(
|
|
63
|
+
'@type': 'Organization',
|
|
64
|
+
name: parent_organization[:name] || parent_organization['name'],
|
|
65
|
+
url: parent_organization[:url] || parent_organization['url']
|
|
66
|
+
)
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PageStructuredData
|
|
4
|
+
module PageTypes
|
|
5
|
+
# schema.org Person structured data.
|
|
6
|
+
class Person
|
|
7
|
+
include SchemaNode
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :url, :image, :same_as
|
|
10
|
+
|
|
11
|
+
def initialize(name:, url: nil, image: nil, same_as: [])
|
|
12
|
+
@name = name
|
|
13
|
+
@url = url
|
|
14
|
+
@image = image
|
|
15
|
+
@same_as = Array(same_as)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
compact_node(
|
|
20
|
+
'@type': 'Person',
|
|
21
|
+
name: name,
|
|
22
|
+
url: url,
|
|
23
|
+
image: image,
|
|
24
|
+
sameAs: same_as
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def warnings
|
|
29
|
+
required_attribute_warnings(name: name)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PageStructuredData
|
|
4
|
+
module PageTypes
|
|
5
|
+
# Shared helpers for schema.org hash values.
|
|
6
|
+
module SchemaNode
|
|
7
|
+
def warnings
|
|
8
|
+
[]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def valid?
|
|
12
|
+
warnings.empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def compact_node(node)
|
|
18
|
+
node.each_with_object({}) do |(key, value), compacted|
|
|
19
|
+
next if blank_schema_value?(value)
|
|
20
|
+
|
|
21
|
+
compacted[key] = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def object_to_h(object)
|
|
26
|
+
return object.to_h if object.respond_to?(:to_h)
|
|
27
|
+
|
|
28
|
+
object
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def blank_schema_value?(value)
|
|
32
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
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
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -4,6 +4,8 @@ module PageStructuredData
|
|
|
4
4
|
module PageTypes
|
|
5
5
|
# WebSite structured data for a page
|
|
6
6
|
class WebSite
|
|
7
|
+
include SchemaNode
|
|
8
|
+
|
|
7
9
|
attr_reader :name, :url, :description, :publisher, :potential_action
|
|
8
10
|
|
|
9
11
|
def initialize(name:, url:, description: nil, publisher: nil, potential_action: nil)
|
|
@@ -15,18 +17,15 @@ module PageStructuredData
|
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def to_h
|
|
18
|
-
|
|
20
|
+
compact_node(
|
|
19
21
|
'@context': 'https://schema.org',
|
|
20
22
|
'@type': 'WebSite',
|
|
21
23
|
name: name,
|
|
22
24
|
url: url,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
node[:potentialAction] = potential_action if potential_action.present?
|
|
28
|
-
|
|
29
|
-
node
|
|
25
|
+
description: description,
|
|
26
|
+
publisher: object_to_h(publisher),
|
|
27
|
+
potentialAction: object_to_h(potential_action)
|
|
28
|
+
)
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
def json_ld
|
|
@@ -37,12 +36,16 @@ module PageStructuredData
|
|
|
37
36
|
)
|
|
38
37
|
end
|
|
39
38
|
|
|
39
|
+
def warnings
|
|
40
|
+
required_attribute_warnings(name: name, url: url) + nested_warnings
|
|
41
|
+
end
|
|
42
|
+
|
|
40
43
|
private
|
|
41
44
|
|
|
42
|
-
def
|
|
43
|
-
return
|
|
45
|
+
def nested_warnings
|
|
46
|
+
return [] unless publisher.respond_to?(:warnings)
|
|
44
47
|
|
|
45
|
-
publisher
|
|
48
|
+
publisher.warnings.map { |warning| "publisher: #{warning}" }
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
end
|
|
@@ -4,23 +4,39 @@
|
|
|
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 %>">
|
|
18
|
+
<% if description.present? %>
|
|
14
19
|
<meta name="description" content="<%= description %>">
|
|
20
|
+
<% end %>
|
|
21
|
+
<% if image.present? %>
|
|
15
22
|
<meta name="image" content="<%= image %>">
|
|
23
|
+
<% end %>
|
|
16
24
|
|
|
17
25
|
<meta property="og:title" content="<%= title %>">
|
|
26
|
+
<% if description.present? %>
|
|
18
27
|
<meta property="og:description" content="<%= description %>">
|
|
28
|
+
<% end %>
|
|
29
|
+
<% if image.present? %>
|
|
19
30
|
<meta property="og:image" content="<%= image %>">
|
|
31
|
+
<% end %>
|
|
20
32
|
|
|
21
33
|
<meta name="twitter:card" content="summary_large_image">
|
|
22
34
|
<meta name="twitter:title" content="<%= title %>">
|
|
35
|
+
<% if description.present? %>
|
|
23
36
|
<meta name="twitter:description" content="<%= description %>">
|
|
37
|
+
<% end %>
|
|
38
|
+
<% if image.present? %>
|
|
24
39
|
<meta name="twitter:image" content="<%= image %>">
|
|
40
|
+
<% end %>
|
|
25
41
|
|
|
26
42
|
<%= page&.json_lds&.html_safe %>
|
data/lib/page_structured_data.rb
CHANGED
|
@@ -2,6 +2,8 @@ require "page_structured_data/version"
|
|
|
2
2
|
require "page_structured_data/engine"
|
|
3
3
|
require_relative "../app/src/page_structured_data/anchors"
|
|
4
4
|
require_relative "../app/src/page_structured_data/breadcrumbs"
|
|
5
|
+
require_relative "../app/src/page_structured_data/page_types/schema_node"
|
|
6
|
+
require_relative "../app/src/page_structured_data/page_types/person"
|
|
5
7
|
require_relative "../app/src/page_structured_data/page_types/interaction_statistic"
|
|
6
8
|
require_relative "../app/src/page_structured_data/page_types/article"
|
|
7
9
|
require_relative "../app/src/page_structured_data/page_types/blog_posting"
|
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
|
-
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: []
|
|
@@ -53,6 +54,8 @@ files:
|
|
|
53
54
|
- app/src/page_structured_data/page_types/interaction_statistic.rb
|
|
54
55
|
- app/src/page_structured_data/page_types/news_article.rb
|
|
55
56
|
- app/src/page_structured_data/page_types/organization.rb
|
|
57
|
+
- app/src/page_structured_data/page_types/person.rb
|
|
58
|
+
- app/src/page_structured_data/page_types/schema_node.rb
|
|
56
59
|
- app/src/page_structured_data/page_types/web_site.rb
|
|
57
60
|
- app/views/page_structured_data/_meta_tags.html.erb
|
|
58
61
|
- config/routes.rb
|
|
@@ -89,5 +92,5 @@ requirements: []
|
|
|
89
92
|
rubygems_version: 3.3.7
|
|
90
93
|
signing_key:
|
|
91
94
|
specification_version: 4
|
|
92
|
-
summary:
|
|
95
|
+
summary: One Rails page object for SEO, social, and JSON-LD metadata
|
|
93
96
|
test_files: []
|