aris 1.4.2 → 1.5.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +251 -0
  3. data/README.md +18 -0
  4. data/docs/ADAPTERS.md +478 -0
  5. data/docs/ARCHITECTURE.md +222 -0
  6. data/docs/CONTENT.md +967 -0
  7. data/docs/PERFORMANCE.md +492 -0
  8. data/docs/PLUGIN_DEVELOPMENT.md +688 -0
  9. data/docs/USAGE.md +4998 -0
  10. data/docs/plugins/API_KEY_AUTH.md +232 -0
  11. data/docs/plugins/BASIC_AUTH.md +582 -0
  12. data/docs/plugins/BEARER_AUTH.md +394 -0
  13. data/docs/plugins/CACHE.md +369 -0
  14. data/docs/plugins/COMPRESSION.md +216 -0
  15. data/docs/plugins/COOKIES.md +30 -0
  16. data/docs/plugins/CORS.md +283 -0
  17. data/docs/plugins/CSRF.md +751 -0
  18. data/docs/plugins/ETAG.md +308 -0
  19. data/docs/plugins/FORM_PARSER.md +193 -0
  20. data/docs/plugins/HEALTH_CHECK.md +469 -0
  21. data/docs/plugins/JSON.md +291 -0
  22. data/docs/plugins/MULTIPART.md +427 -0
  23. data/docs/plugins/RATE_LIMITER.md +368 -0
  24. data/docs/plugins/REQUEST_ID.md +369 -0
  25. data/docs/plugins/REQUEST_LOGGER.md +151 -0
  26. data/docs/plugins/SECURITY.md +193 -0
  27. data/docs/plugins/SESSION.md +98 -0
  28. data/lib/aris/adapters/rack/adapter.rb +17 -2
  29. data/lib/aris/adapters/rack/request.rb +29 -11
  30. data/lib/aris/plugins/basic_auth.rb +3 -1
  31. data/lib/aris/plugins/cookies.rb +4 -32
  32. data/lib/aris/plugins/cors.rb +8 -1
  33. data/lib/aris/plugins/csrf.rb +63 -22
  34. data/lib/aris/plugins/flash.rb +3 -1
  35. data/lib/aris/plugins/form_parser.rb +52 -31
  36. data/lib/aris/plugins/multipart.rb +22 -2
  37. data/lib/aris/plugins/request_logger.rb +8 -1
  38. data/lib/aris/plugins/security_headers.rb +8 -1
  39. data/lib/aris/plugins/session.rb +150 -99
  40. data/lib/aris/response_helpers.rb +41 -0
  41. data/lib/aris/version.rb +2 -2
  42. metadata +31 -3
data/docs/CONTENT.md ADDED
@@ -0,0 +1,967 @@
1
+ # Aris Content - Complete Specification
2
+
3
+ NOTE!: This is yet to be implemented. Features are currently onin flux.
4
+
5
+ ## Overview
6
+
7
+ **aris-content** is a companion gem for **aris** that provides content intelligence and SEO utilities for multi-domain, static-first Ruby sites. It operates in two silos:
8
+
9
+ 1. **Hub** - Development-time feedback, metrics, and insights
10
+ 2. **Generators** - Production-ready SEO utilities (sitemap, robots, meta, schema)
11
+
12
+ **Philosophy:**
13
+ - Declarative at the handler level
14
+ - Zero performance impact on production requests
15
+ - Works with both file-based discovery and hash-based routes
16
+ - Unix philosophy: optional, composable, focused
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ ```ruby
23
+ # Gemfile
24
+ gem 'aris' # Core router
25
+ gem 'aris-content' # Content intelligence
26
+
27
+ # Auto-loads and extends Aris with content features
28
+ ```
29
+
30
+ ---
31
+
32
+ ## SILO 1: Hub (Development Intelligence)
33
+
34
+ ### Architecture
35
+
36
+ - SQLite database (`.aris/content.db`) - gitignored
37
+ - Git integration for freshness tracking
38
+ - File watcher for live updates
39
+ - Separate Rack server for dashboard
40
+ - Zero impact on main app performance
41
+
42
+ ### Features
43
+
44
+ #### 1. Content Indexer
45
+ Scans all routes (discovered + hash-based) and extracts:
46
+ - Domain, path, method
47
+ - Handler metadata (meta, sitemap, schema, redirects)
48
+ - Content analysis (word count, keywords, headings)
49
+ - Performance metrics (HTML size, image count)
50
+ - Link structure (internal/external)
51
+
52
+ #### 2. Health Score
53
+ Aggregate score (0-100) based on:
54
+ - Meta tags present
55
+ - Content freshness
56
+ - Performance budgets met
57
+ - Link graph health
58
+ - Image optimization
59
+ - Heading structure
60
+
61
+ #### 3. Freshness Tracker
62
+ Uses Git to track:
63
+ - Last updated timestamp
64
+ - Author of last change
65
+ - Commit history
66
+ - Time since last update
67
+ - Stale content warnings (configurable threshold)
68
+
69
+ #### 4. Performance Budgets
70
+ Tracks and warns:
71
+ - HTML size
72
+ - Total page weight
73
+ - Image count/size
74
+ - External requests
75
+ - Custom metrics
76
+
77
+ #### 5. Link Graph Analysis
78
+ - Internal link structure
79
+ - Orphaned pages (no incoming links)
80
+ - Deep pages (5+ clicks from home)
81
+ - Broken internal links
82
+ - Most/least linked pages
83
+ - Link graph visualization
84
+
85
+ #### 6. SEO Validation
86
+ - Missing meta tags
87
+ - Duplicate titles/descriptions
88
+ - Heading hierarchy issues (H1/H2/H3)
89
+ - Image alt text missing
90
+ - Canonical issues
91
+ - Schema.org validation
92
+
93
+ #### 7. Content Analysis
94
+ - Word count
95
+ - Reading time
96
+ - Keyword extraction & frequency
97
+ - Top keywords per page
98
+ - Content comparison tool
99
+
100
+ #### 8. Dashboard Server
101
+ - Clean HTML interface (Alpine.js for interactivity)
102
+ - Issue list (critical/warning/info)
103
+ - Per-page detail views
104
+ - Quick wins recommendations
105
+ - Auto-refresh on file changes
106
+ - Export to JSON
107
+
108
+ ---
109
+
110
+ ### Hub CLI Commands
111
+
112
+ ```bash
113
+ # Start dashboard server
114
+ $ aris content hub
115
+ 🚀 Content Hub starting...
116
+ 📊 Dashboard: http://localhost:4000
117
+ 🔍 Analyzing 247 pages across 3 domains...
118
+ ✅ Ready in 2.1s
119
+
120
+ # Quick status check
121
+ $ aris content status
122
+ Health Score: 87/100
123
+ Critical Issues: 4
124
+ Warnings: 12
125
+ Pages: 247
126
+
127
+ # List stale content
128
+ $ aris content stale
129
+ /blog/old-post (18 months ago)
130
+ /about/team (14 months ago)
131
+ /services/legacy (9 months ago)
132
+
133
+ # Find orphaned pages
134
+ $ aris content orphans
135
+ /hidden/page (0 incoming links)
136
+ /test/sandbox (0 incoming links)
137
+
138
+ # Performance budget check
139
+ $ aris content budgets
140
+ ❌ /blog/post-1 (89kb > 50kb budget)
141
+ ⚠️ /products (52kb > 50kb budget)
142
+ ✅ 245 pages within budget
143
+
144
+ # Export data
145
+ $ aris content export --format json > content-report.json
146
+
147
+ # Compare two pages
148
+ $ aris content compare /blog/post-1 /blog/post-2
149
+ Word count: 1,234 vs 456
150
+ Keywords: ruby(12) vs rails(8)
151
+ Links out: 5 vs 2
152
+ Images: 3 vs 1
153
+ Read time: 6min vs 2min
154
+ ```
155
+
156
+ ---
157
+
158
+ ### Hub Configuration
159
+
160
+ ```ruby
161
+ # config/aris_content.rb
162
+ Aris::Content.configure do |config|
163
+ # Performance budgets
164
+ config.performance_budget do |budget|
165
+ budget.max_html_size = 50.kilobytes
166
+ budget.max_images = 10
167
+ budget.max_external_requests = 20
168
+ end
169
+
170
+ # Freshness thresholds
171
+ config.freshness_threshold = 6.months
172
+ config.freshness_warning = 3.months
173
+
174
+ # Target keywords per domain
175
+ config.target_keywords "example.com", %w[ruby router performance seo]
176
+ config.target_keywords "shop.example.com", %w[products ecommerce checkout]
177
+
178
+ # Dashboard settings
179
+ config.dashboard_port = 4000
180
+ config.dashboard_refresh_interval = 5.seconds
181
+
182
+ # Exclude paths from analysis
183
+ config.exclude_paths %w[/admin /api/internal /test]
184
+ end
185
+ ```
186
+
187
+ ---
188
+
189
+ ## SILO 2: Generators (SEO Utilities)
190
+
191
+ ### 1. Sitemap Generator
192
+
193
+ #### Handler DSL
194
+
195
+ ```ruby
196
+ # File-based route
197
+ module Handler
198
+ extend Aris::RouteHelpers
199
+
200
+ sitemap(
201
+ priority: 0.8,
202
+ changefreq: 'weekly',
203
+ lastmod: '2025-01-15'
204
+ )
205
+
206
+ def self.call(request, params)
207
+ # ...
208
+ end
209
+ end
210
+ ```
211
+
212
+ #### Dynamic Sitemap (Multiple URLs per Route)
213
+
214
+ ```ruby
215
+ sitemap do
216
+ # Return array of URL data
217
+ Post.all.map do |post|
218
+ {
219
+ path: "/blog/#{post.slug}",
220
+ priority: post.featured? ? 1.0 : 0.7,
221
+ changefreq: 'daily',
222
+ lastmod: post.updated_at
223
+ }
224
+ end
225
+ end
226
+ ```
227
+
228
+ #### Hash-based Routes
229
+
230
+ ```ruby
231
+ Aris.routes({
232
+ "example.com": {
233
+ "/": {
234
+ get: {
235
+ to: HomeHandler,
236
+ sitemap: { priority: 1.0, changefreq: 'daily' }
237
+ }
238
+ },
239
+ "/about": {
240
+ get: {
241
+ to: AboutHandler,
242
+ sitemap: { priority: 0.8, changefreq: 'monthly' }
243
+ }
244
+ }
245
+ }
246
+ })
247
+ ```
248
+
249
+ #### Auto-serves `/sitemap.xml`
250
+
251
+ ```xml
252
+ GET /sitemap.xml
253
+
254
+ <?xml version="1.0" encoding="UTF-8"?>
255
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
256
+ <url>
257
+ <loc>https://example.com/</loc>
258
+ <lastmod>2025-01-15</lastmod>
259
+ <changefreq>daily</changefreq>
260
+ <priority>1.0</priority>
261
+ </url>
262
+ <url>
263
+ <loc>https://example.com/about</loc>
264
+ <changefreq>monthly</changefreq>
265
+ <priority>0.8</priority>
266
+ </url>
267
+ </urlset>
268
+ ```
269
+
270
+ ---
271
+
272
+ ### 2. Robots.txt Generator
273
+
274
+ #### Handler DSL
275
+
276
+ ```ruby
277
+ robots(
278
+ allow: ["/", "/blog/*"],
279
+ disallow: ["/admin", "/api/*", "/private"],
280
+ crawl_delay: 1
281
+ )
282
+ ```
283
+
284
+ #### Domain-level Configuration
285
+
286
+ ```ruby
287
+ Aris::Content::Robots.configure do |config|
288
+ config.domain "example.com" do
289
+ allow ["/", "/blog/*", "/products/*"]
290
+ disallow ["/admin", "/private"]
291
+ sitemap "https://example.com/sitemap.xml"
292
+ end
293
+
294
+ config.domain "staging.example.com" do
295
+ disallow ["/"] # Block all on staging
296
+ end
297
+ end
298
+ ```
299
+
300
+ #### Auto-serves `/robots.txt`
301
+
302
+ ```
303
+ GET /robots.txt
304
+
305
+ User-agent: *
306
+ Allow: /
307
+ Allow: /blog/*
308
+ Disallow: /admin
309
+ Disallow: /api/*
310
+ Crawl-delay: 1
311
+
312
+ Sitemap: https://example.com/sitemap.xml
313
+ ```
314
+
315
+ ---
316
+
317
+ ### 3. Meta Tags Manager
318
+
319
+ #### Handler DSL
320
+
321
+ ```ruby
322
+ meta(
323
+ title: "Best Ruby Framework 2025",
324
+ description: "Deploy sites across multiple domains with lightning speed",
325
+ keywords: %w[ruby router framework multi-domain],
326
+
327
+ # Open Graph
328
+ og_title: "Custom OG Title",
329
+ og_description: "Custom OG Description",
330
+ og_image: "/images/og-hero.jpg",
331
+ og_type: "website",
332
+
333
+ # Twitter Card
334
+ twitter_card: "summary_large_image",
335
+ twitter_site: "@example",
336
+ twitter_creator: "@author",
337
+
338
+ # SEO
339
+ canonical: true, # Auto-generates from current path
340
+ robots: "index, follow"
341
+ )
342
+ ```
343
+
344
+ #### Dynamic Meta
345
+
346
+ ```ruby
347
+ meta do
348
+ {
349
+ title: "#{@product.name} | Example Store",
350
+ description: @product.description.truncate(160),
351
+ og_image: @product.image_url,
352
+ og_type: "product"
353
+ }
354
+ end
355
+ ```
356
+
357
+ #### Hash-based Routes
358
+
359
+ ```ruby
360
+ Aris.routes({
361
+ "example.com": {
362
+ "/": {
363
+ get: {
364
+ to: HomeHandler,
365
+ meta: {
366
+ title: "Welcome Home",
367
+ description: "...",
368
+ og_image: "/hero.jpg"
369
+ }
370
+ }
371
+ }
372
+ }
373
+ })
374
+ ```
375
+
376
+ #### Rendering in Layout
377
+
378
+ ```erb
379
+ <!-- layout.html.erb -->
380
+ <head>
381
+ <%= Aris::Content::Meta.render %>
382
+ </head>
383
+ ```
384
+
385
+ **Outputs:**
386
+
387
+ ```html
388
+ <title>Best Ruby Framework 2025</title>
389
+ <meta name="description" content="Deploy sites across multiple domains...">
390
+ <meta name="keywords" content="ruby, router, framework, multi-domain">
391
+
392
+ <meta property="og:title" content="Custom OG Title">
393
+ <meta property="og:description" content="Custom OG Description">
394
+ <meta property="og:image" content="https://example.com/images/og-hero.jpg">
395
+ <meta property="og:type" content="website">
396
+ <meta property="og:url" content="https://example.com/current-page">
397
+
398
+ <meta name="twitter:card" content="summary_large_image">
399
+ <meta name="twitter:site" content="@example">
400
+ <meta name="twitter:creator" content="@author">
401
+
402
+ <link rel="canonical" href="https://example.com/current-page">
403
+ <meta name="robots" content="index, follow">
404
+ ```
405
+
406
+ ---
407
+
408
+ ### 4. Structured Data (Schema.org)
409
+
410
+ #### Handler DSL
411
+
412
+ ```ruby
413
+ schema(
414
+ type: "Article",
415
+ headline: "My Amazing Blog Post",
416
+ author: {
417
+ "@type": "Person",
418
+ name: "John Doe"
419
+ },
420
+ datePublished: "2025-01-15",
421
+ dateModified: "2025-01-20",
422
+ image: "/images/post-hero.jpg",
423
+ publisher: {
424
+ "@type": "Organization",
425
+ name: "Example Company",
426
+ logo: "/logo.png"
427
+ }
428
+ )
429
+ ```
430
+
431
+ #### Dynamic Schema
432
+
433
+ ```ruby
434
+ schema do
435
+ {
436
+ "@type": "Product",
437
+ name: @product.name,
438
+ description: @product.description,
439
+ image: @product.image_url,
440
+ offers: {
441
+ "@type": "Offer",
442
+ price: @product.price,
443
+ priceCurrency: "USD",
444
+ availability: @product.in_stock? ? "InStock" : "OutOfStock"
445
+ }
446
+ }
447
+ end
448
+ ```
449
+
450
+ #### Built-in Templates
451
+
452
+ ```ruby
453
+ schema :article do |s|
454
+ s.headline = "My Post"
455
+ s.author = "John Doe"
456
+ s.date_published = "2025-01-15"
457
+ end
458
+
459
+ schema :product do |s|
460
+ s.name = @product.name
461
+ s.price = @product.price
462
+ s.image = @product.image_url
463
+ end
464
+
465
+ schema :organization do |s|
466
+ s.name = "Example Company"
467
+ s.url = "https://example.com"
468
+ s.logo = "/logo.png"
469
+ s.social_profiles = [
470
+ "https://twitter.com/example",
471
+ "https://facebook.com/example"
472
+ ]
473
+ end
474
+ ```
475
+
476
+ #### Rendering in Layout
477
+
478
+ ```erb
479
+ <head>
480
+ <%= Aris::Content::Schema.render %>
481
+ </head>
482
+ ```
483
+
484
+ **Outputs:**
485
+
486
+ ```html
487
+ <script type="application/ld+json">
488
+ {
489
+ "@context": "https://schema.org",
490
+ "@type": "Article",
491
+ "headline": "My Amazing Blog Post",
492
+ "author": {
493
+ "@type": "Person",
494
+ "name": "John Doe"
495
+ },
496
+ "datePublished": "2025-01-15",
497
+ "dateModified": "2025-01-20",
498
+ "image": "https://example.com/images/post-hero.jpg",
499
+ "publisher": {
500
+ "@type": "Organization",
501
+ "name": "Example Company",
502
+ "logo": "https://example.com/logo.png"
503
+ }
504
+ }
505
+ </script>
506
+ ```
507
+
508
+ ---
509
+
510
+ ### 5. RSS/Atom Feed Generator
511
+
512
+ #### Configuration
513
+
514
+ ```ruby
515
+ Aris::Content::Feed.configure do |config|
516
+ config.domain "example.com" do
517
+ title "Example Blog"
518
+ description "Latest posts from Example"
519
+ link "https://example.com"
520
+
521
+ items do
522
+ Post.recent(20).map do |post|
523
+ {
524
+ title: post.title,
525
+ link: "https://example.com/blog/#{post.slug}",
526
+ description: post.excerpt,
527
+ pubDate: post.published_at,
528
+ guid: post.id
529
+ }
530
+ end
531
+ end
532
+ end
533
+ end
534
+ ```
535
+
536
+ #### Auto-serves `/feed.xml`
537
+
538
+ ```xml
539
+ GET /feed.xml
540
+
541
+ <?xml version="1.0" encoding="UTF-8"?>
542
+ <rss version="2.0">
543
+ <channel>
544
+ <title>Example Blog</title>
545
+ <link>https://example.com</link>
546
+ <description>Latest posts from Example</description>
547
+ <item>
548
+ <title>My Blog Post</title>
549
+ <link>https://example.com/blog/my-post</link>
550
+ <description>Post excerpt...</description>
551
+ <pubDate>Mon, 15 Jan 2025 10:00:00 GMT</pubDate>
552
+ <guid>123</guid>
553
+ </item>
554
+ </channel>
555
+ </rss>
556
+ ```
557
+
558
+ #### Auto-injects Feed Link
559
+
560
+ ```html
561
+ <head>
562
+ <link rel="alternate" type="application/rss+xml"
563
+ title="Example Blog"
564
+ href="https://example.com/feed.xml">
565
+ </head>
566
+ ```
567
+
568
+ ---
569
+
570
+ ### 6. Breadcrumbs Generator
571
+
572
+ #### Handler DSL
573
+
574
+ ```ruby
575
+ breadcrumbs auto: true # Generates from route structure
576
+
577
+ # Or custom
578
+ breadcrumbs [
579
+ { name: "Home", url: "/" },
580
+ { name: "Blog", url: "/blog" },
581
+ { name: "My Post" } # Current page, no URL
582
+ ]
583
+ ```
584
+
585
+ #### Rendering
586
+
587
+ ```erb
588
+ <%= Aris::Content::Breadcrumbs.render %>
589
+ ```
590
+
591
+ **Outputs:**
592
+
593
+ ```html
594
+ <nav aria-label="breadcrumb">
595
+ <ol itemscope itemtype="https://schema.org/BreadcrumbList">
596
+ <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
597
+ <a itemprop="item" href="/">
598
+ <span itemprop="name">Home</span>
599
+ </a>
600
+ <meta itemprop="position" content="1">
601
+ </li>
602
+ <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
603
+ <a itemprop="item" href="/blog">
604
+ <span itemprop="name">Blog</span>
605
+ </a>
606
+ <meta itemprop="position" content="2">
607
+ </li>
608
+ <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
609
+ <span itemprop="name">My Post</span>
610
+ <meta itemprop="position" content="3">
611
+ </li>
612
+ </ol>
613
+ </nav>
614
+
615
+ <script type="application/ld+json">
616
+ {
617
+ "@context": "https://schema.org",
618
+ "@type": "BreadcrumbList",
619
+ "itemListElement": [...]
620
+ }
621
+ </script>
622
+ ```
623
+
624
+ ---
625
+
626
+ ## Integration with Aris Core
627
+
628
+ ### Automatic Hook Points
629
+
630
+ ```ruby
631
+ # In aris-content, automatically extends:
632
+
633
+ # 1. RouteHelpers module
634
+ module Aris::RouteHelpers
635
+ # Adds: sitemap, meta, schema, robots, breadcrumbs methods
636
+ end
637
+
638
+ # 2. Discovery system
639
+ module Aris::Discovery
640
+ # Hooks into add_route_to_hash to register metadata
641
+ end
642
+
643
+ # 3. Compiler
644
+ module Aris
645
+ # Hooks into routes() to extract metadata from hash routes
646
+ end
647
+
648
+ # 4. Base adapter
649
+ module Aris::Adapters::Base
650
+ # Adds: handle_sitemap, handle_robots, handle_feed methods
651
+ end
652
+ ```
653
+
654
+ ### Zero Configuration Required
655
+
656
+ ```ruby
657
+ # Just require the gem
658
+ require 'aris/content'
659
+
660
+ # Everything works automatically:
661
+ # - Handlers can use sitemap/meta/schema
662
+ # - /sitemap.xml auto-serves
663
+ # - /robots.txt auto-serves
664
+ # - /feed.xml auto-serves
665
+ # - Hub commands available
666
+ ```
667
+
668
+ ---
669
+
670
+ ## Production Metrics (Optional Plugin)
671
+
672
+ ### Opt-in Tracking
673
+
674
+ ```ruby
675
+ # Gemfile
676
+ gem 'aris-content', require: ['aris/content', 'aris/content/metrics']
677
+
678
+ # config.ru (production)
679
+ use Aris::Content::MetricsCollector,
680
+ storage: './metrics.db',
681
+ sample_rate: 0.1 # Track 10% of requests
682
+
683
+ run Aris::Adapters::RackApp.new
684
+ ```
685
+
686
+ ### What's Tracked (Minimal, Privacy-Focused)
687
+
688
+ - Path
689
+ - Domain
690
+ - Status code
691
+ - Response time
692
+ - Timestamp
693
+ - **NO user data, NO IP addresses, NO PII**
694
+
695
+ ### Pull to Dev
696
+
697
+ ```bash
698
+ # Export from production
699
+ $ aris content export-metrics > metrics.json
700
+
701
+ # Import to dev
702
+ $ aris content import-metrics metrics.json
703
+
704
+ # Dashboard now shows production data
705
+ ```
706
+
707
+ **Hub Dashboard with Production Data:**
708
+
709
+ ```
710
+ TOP PAGES (Last 30 Days)
711
+
712
+ Production Data: ✓
713
+
714
+ Path Views Avg Time Health
715
+ / 45,231 120ms ✅ 95
716
+ /blog/popular-post 12,450 340ms ⚠️ 72
717
+ /products 8,932 890ms ❌ 45 (slow!)
718
+ ```
719
+
720
+ ---
721
+
722
+ ## Developer Experience Examples
723
+
724
+ ### Example 1: Blog Post with Full SEO
725
+
726
+ ```ruby
727
+ # app/routes/example.com/blog/:slug/get.rb
728
+ module Handler
729
+ extend Aris::RouteHelpers
730
+
731
+ # Sitemap
732
+ sitemap do
733
+ Post.published.map do |post|
734
+ {
735
+ path: "/blog/#{post.slug}",
736
+ priority: post.featured? ? 1.0 : 0.7,
737
+ changefreq: 'weekly',
738
+ lastmod: post.updated_at
739
+ }
740
+ end
741
+ end
742
+
743
+ # Meta tags
744
+ meta do
745
+ {
746
+ title: "#{@post.title} | Example Blog",
747
+ description: @post.excerpt.truncate(160),
748
+ keywords: @post.tags,
749
+ og_image: @post.hero_image_url,
750
+ og_type: "article",
751
+ twitter_card: "summary_large_image"
752
+ }
753
+ end
754
+
755
+ # Structured data
756
+ schema :article do |s|
757
+ s.headline = @post.title
758
+ s.author = @post.author.name
759
+ s.date_published = @post.published_at
760
+ s.date_modified = @post.updated_at
761
+ s.image = @post.hero_image_url
762
+ end
763
+
764
+ # Breadcrumbs
765
+ breadcrumbs [
766
+ { name: "Home", url: "/" },
767
+ { name: "Blog", url: "/blog" },
768
+ { name: @post.title }
769
+ ]
770
+
771
+ def self.call(request, params)
772
+ @post = Post.find_by_slug(params[:slug])
773
+ render :blog_post
774
+ end
775
+ end
776
+ ```
777
+
778
+ ### Example 2: Product Page
779
+
780
+ ```ruby
781
+ # app/routes/shop.example.com/products/:id/get.rb
782
+ module Handler
783
+ extend Aris::RouteHelpers
784
+
785
+ sitemap(priority: 0.8, changefreq: 'daily')
786
+
787
+ meta do
788
+ {
789
+ title: "#{@product.name} - $#{@product.price}",
790
+ description: @product.description,
791
+ og_type: "product",
792
+ og_image: @product.primary_image_url
793
+ }
794
+ end
795
+
796
+ schema :product do |s|
797
+ s.name = @product.name
798
+ s.description = @product.description
799
+ s.image = @product.images.map(&:url)
800
+ s.offers do
801
+ {
802
+ "@type": "Offer",
803
+ price: @product.price,
804
+ priceCurrency: "USD",
805
+ availability: @product.in_stock? ? "InStock" : "OutOfStock"
806
+ }
807
+ end
808
+ end
809
+
810
+ def self.call(request, params)
811
+ @product = Product.find(params[:id])
812
+ render :product
813
+ end
814
+ end
815
+ ```
816
+
817
+ ### Example 3: Simple Page (Hash Routes)
818
+
819
+ ```ruby
820
+ Aris.routes({
821
+ "example.com": {
822
+ "/about": {
823
+ get: {
824
+ to: AboutHandler,
825
+ sitemap: { priority: 0.8, changefreq: 'monthly' },
826
+ meta: {
827
+ title: "About Us",
828
+ description: "Learn about our company",
829
+ og_image: "/images/about-hero.jpg"
830
+ }
831
+ }
832
+ }
833
+ }
834
+ })
835
+ ```
836
+
837
+ ---
838
+
839
+ ## Dashboard Preview
840
+
841
+ ```
842
+ ╔════════════════════════════════════════════════════════╗
843
+ ║ CONTENT HEALTH SCORE: 87/100 ║
844
+ ║ Last updated: 3 seconds ago ║
845
+ ╚════════════════════════════════════════════════════════╝
846
+
847
+ 🔴 CRITICAL ISSUES (4)
848
+
849
+ /blog/old-post (example.com)
850
+ ├─ Not updated in 18 months
851
+ ├─ No internal links to this page
852
+ ├─ Missing meta description
853
+ └─ [View Details] [Mark Reviewed]
854
+
855
+ /products/widget (shop.example.com)
856
+ ├─ HTML size: 89kb (budget: 50kb)
857
+ ├─ 3 images without alt text
858
+ ├─ hero.jpg is 2.1MB (not optimized)
859
+ └─ [View Details] [Ignore]
860
+
861
+ ⚠️ WARNINGS (12)
862
+
863
+ /about/team (example.com)
864
+ ├─ Title is short (15 chars, recommend 50-60)
865
+ ├─ Last updated 5 months ago
866
+ └─ [View Details]
867
+
868
+ ✅ HEALTHY (231 pages)
869
+
870
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
871
+
872
+ QUICK WINS (Easy fixes, high impact)
873
+
874
+ 1. Add meta descriptions (8 pages)
875
+ 2. Optimize large images (12 images)
876
+ 3. Fix duplicate titles (3 pages)
877
+ 4. Link to orphaned pages (3 pages)
878
+
879
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
880
+
881
+ PERFORMANCE BUDGETS
882
+
883
+ example.com
884
+ ├─ / ...................... ✅ 24kb (budget: 50kb)
885
+ ├─ /about ................. ✅ 31kb (budget: 50kb)
886
+ ├─ /blog/post-1 ........... ❌ 89kb (budget: 50kb)
887
+ └─ /contact ............... ✅ 18kb (budget: 50kb)
888
+
889
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
890
+
891
+ CONTENT FRESHNESS
892
+
893
+ 🔴 STALE (6+ months) - 4 pages
894
+ 🟡 AGING (3-6 months) - 12 pages
895
+ 🟢 FRESH (<3 months) - 231 pages
896
+
897
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
898
+
899
+ LINK GRAPH
900
+
901
+ Orphaned Pages: 3
902
+ Deep Pages (5+ clicks): 7
903
+ Broken Links: 2
904
+
905
+ Most Linked:
906
+ 1. /blog/ultimate-guide (47 links)
907
+ 2. / (38 links)
908
+ 3. /products (31 links)
909
+
910
+ [View Full Graph]
911
+ ```
912
+
913
+ ---
914
+
915
+ ## Roadmap
916
+
917
+ ### v0.1 (MVP)
918
+ - Content indexer
919
+ - Freshness tracker
920
+ - Performance budgets
921
+ - Link graph analysis
922
+ - Basic dashboard
923
+ - Sitemap generator
924
+ - robots.txt generator
925
+ - Meta tags manager
926
+
927
+ ### v0.2
928
+ - Schema.org support
929
+ - Image analysis
930
+ - Heading validator
931
+ - RSS/Atom feeds
932
+ - Breadcrumbs
933
+
934
+ ### v0.3
935
+ - Keyword extraction
936
+ - Content comparison
937
+ - Production metrics (opt-in)
938
+ - Dashboard improvements
939
+
940
+ ### v1.0
941
+ - Content templates
942
+ - A/B test tracking
943
+ - Trend analysis
944
+ - Advanced visualizations
945
+ - Team features (maybe)
946
+
947
+ ---
948
+
949
+ ## Summary
950
+
951
+ **aris-content** turns content management into a joy by:
952
+
953
+ 1. **Showing you problems** before they hurt SEO
954
+ 2. **Making SEO declarative** at the handler level
955
+ 3. **Tracking freshness** automatically via Git
956
+ 4. **Enforcing budgets** so pages stay fast
957
+ 5. **Finding weak spots** in your content structure
958
+ 6. **Generating everything** (sitemap, robots, meta, schema)
959
+ 7. **Zero production cost** - it's all dev-time or generated
960
+
961
+ **The DX is chef's kiss:** Write `sitemap`, `meta`, `schema` in your handler, and everything just works. No separate config files, no manual XML editing, no third-party tools.
962
+
963
+ **This doesn't exist anywhere else.** Not in Rails, not in Sinatra, not in JS frameworks, not in PHP. This is your competitive advantage as a solo dev managing multiple domains.
964
+
965
+ ---
966
+
967
+ Ready to build? 🚀