eksa-framework 2.2.2 โ†’ 3.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d6f7016cde9ee0b7fdc950e4ae0de7cee2425ffde98f2a3879679ce7b685677
4
- data.tar.gz: 6e6c5b4535d9f08b0f7dd486e4641de30b4df019d1fce4a171f3203bb7bfe07b
3
+ metadata.gz: 780c728ee88c41e54a84523f1d70bec838d559c165322ef92afadd4623f2e1b4
4
+ data.tar.gz: 3096a17f07c232b8b8c80a706e51310f3fc6196077877390c46b7e1fe4f802cb
5
5
  SHA512:
6
- metadata.gz: 12e84b512d1b40e62b47dcd10fa06334a2ae49103e251bfa4f1d36df607193b5719563aed61d25a84c252dd8c2cfb55aebcd0868185b1968dcfbdc46c6b6a2f3
7
- data.tar.gz: 9704f8011116a0d726685e35f8540e1bd592d29e35db3c3ddcaab03b960fc474c52ef46e44ee07b5757ee539a044e8beba3290eba06874d5821db7b90b4c901a
6
+ metadata.gz: 96c60d22e3632b4a08402c1e4a3b4ad5cbd82c3e209b87d30725cb7de9fb6dd4cd43b61eea09c62378cced2696286efaf199c70824521c3968ef775688feeed8
7
+ data.tar.gz: d2ecc1406064d10b52d08763b0e72dc6b65c3fa2b8c97dc16141ec57c0b6e526ed537da964b827c7bf460e28c430727963338c14993038d04e98e4b059cc2bda
data/Gemfile CHANGED
@@ -8,4 +8,6 @@ end
8
8
  gem 'rack'
9
9
  gem 'puma'
10
10
  gem "rackup"
11
- gem 'sqlite3'
11
+ gem 'sqlite3'
12
+ gem 'kramdown'
13
+ gem 'kramdown-parser-gfm'
data/Gemfile.lock CHANGED
@@ -2,6 +2,10 @@ GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
4
  diff-lcs (1.6.2)
5
+ kramdown (2.5.2)
6
+ rexml (>= 3.4.4)
7
+ kramdown-parser-gfm (1.1.0)
8
+ kramdown (~> 2.0)
5
9
  nio4r (2.7.5)
6
10
  puma (7.2.0)
7
11
  nio4r (~> 2.0)
@@ -10,6 +14,7 @@ GEM
10
14
  rack (>= 1.3)
11
15
  rackup (2.3.1)
12
16
  rack (>= 3)
17
+ rexml (3.4.4)
13
18
  rspec (3.13.2)
14
19
  rspec-core (~> 3.13.0)
15
20
  rspec-expectations (~> 3.13.0)
@@ -29,6 +34,8 @@ PLATFORMS
29
34
  x86_64-linux-gnu
30
35
 
31
36
  DEPENDENCIES
37
+ kramdown
38
+ kramdown-parser-gfm
32
39
  puma
33
40
  rack
34
41
  rack-test (~> 2.1)
@@ -38,11 +45,14 @@ DEPENDENCIES
38
45
 
39
46
  CHECKSUMS
40
47
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
48
+ kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa
49
+ kramdown-parser-gfm (1.1.0) sha256=fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729
41
50
  nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
42
51
  puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
43
52
  rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
44
53
  rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
45
54
  rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
55
+ rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
46
56
  rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
47
57
  rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
48
58
  rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
data/README.md CHANGED
@@ -18,6 +18,7 @@
18
18
  * ๐Ÿงช **Built-in Testing**: Lingkungan pengujian otomatis siap pakai menggunakan RSpec dan `rack-test`.
19
19
  * ๐ŸŽจ **Asset Helpers**: Library bawaan untuk pengelolaan CSS dan JS yang lebih rapi.
20
20
  * ๐Ÿ” **Dynamic SEO Engine**: Penanganan otomatis file `robots.txt` dan `sitemap.xml`.
21
+ * ๐Ÿ’Ž **JSON-LD Support**: Dukungan data terstruktur (Structured Data) otomatis untuk SEO yang lebih optimal.
21
22
  * ๐Ÿ‘ป **Aesthetic Error Pages**: Halaman 404 dengan desain Glassmorphism yang elegan secara native.
22
23
 
23
24
  ---
@@ -66,9 +67,29 @@ eksa g controller Blog
66
67
 
67
68
  # Membuat model dan schema database
68
69
  eksa g model Post
70
+
71
+ # Membuat postingan blog baru
72
+ eksa g post "Judul Artikel"
73
+ ```
74
+
75
+ ### 3. Markdown Blog Engine
76
+ Eksa memiliki sistem blog bawaan yang cara kerjanya mirip Jekyll. Cukup buat file `.md` di folder `_posts/` dengan metadata YAML (Front Matter):
77
+
78
+ ```markdown
79
+ ---
80
+ title: "Halo Eksa"
81
+ date: 2026-03-15 14:00:00
82
+ ---
83
+
84
+ Isi konten blog menggunakan **Markdown**.
69
85
  ```
70
86
 
71
- ### 3. Database & Model
87
+ Fitur Blog:
88
+ * **Dynamic Slug**: Otomatis mengenali rute `/posts/:slug`.
89
+ * **Syntax Highlighting**: Kode di dalam blog otomatis berwarna & punya tombol copy.
90
+ * **Aesthetic UI**: Template blog bawaan dengan desain Glassmorphism.
91
+
92
+ ### 4. Database & Model
72
93
  Definisikan schema tabel Anda langsung di dalam model:
73
94
 
74
95
  ```ruby
@@ -103,4 +124,4 @@ bundle exec rspec
103
124
  ---
104
125
 
105
126
  ## ๐Ÿ“œ Lisensi
106
- Proyek ini dilisensikan di bawah **MIT License**. Lihat file [LICENSE](LICENSE) untuk detail lebih lanjut.
127
+ Proyek ini dilisensikan di bawah **MIT License**. Lihat file [LICENSE](https://github.com/IshikawaUta/eksa-framework/blob/main/LICENSE) untuk detail lebih lanjut.
@@ -0,0 +1,126 @@
1
+ ---
2
+ title: "Welcome To Eksa Framework"
3
+ date: 2026-03-15 14:09:58
4
+ ---
5
+
6
+ # โœจ Eksa Framework
7
+
8
+ **Eksa Framework** adalah *micro-framework* MVC (Model-View-Controller) modern yang dibangun di atas Ruby dan Rack. Didesain untuk pengembang yang menginginkan kecepatan, kode yang bersih, dan tampilan antarmuka **Glassmorphism** yang elegan secara *out-of-the-box*.
9
+
10
+ ---
11
+
12
+ ## ๐Ÿš€ Fitur Unggulan
13
+
14
+ * ๐Ÿ’Ž **Modern Glassmorphism UI**: Tampilan transparan yang indah dengan Tailwind CSS & Lucide Icons.
15
+ * โšก **Rack 3 & Middleware Support**: Mendukung standar terbaru dan pembuatan pipeline middleware kustom.
16
+ * ๐Ÿ› ๏ธ **Powerful CLI**: Inisialisasi project (`eksa init`), jalankan server (`eksa run`), generate komponen, dan **auto-routing** otomatis.
17
+ * ๐Ÿ’พ **Dynamic Database Engine**: Database SQLite otomatis dengan schema yang ditentukan oleh model Anda sendiri.
18
+ * ๐Ÿงช **Built-in Testing**: Lingkungan pengujian otomatis siap pakai menggunakan RSpec dan `rack-test`.
19
+ * ๐ŸŽจ **Asset Helpers**: Library bawaan untuk pengelolaan CSS dan JS yang lebih rapi.
20
+ * ๐Ÿ” **Dynamic SEO Engine**: Penanganan otomatis file `robots.txt` dan `sitemap.xml`.
21
+ * ๐Ÿ‘ป **Aesthetic Error Pages**: Halaman 404 dengan desain Glassmorphism yang elegan secara native.
22
+
23
+ ---
24
+
25
+ ## ๐Ÿ› ๏ธ Instalasi Cepat
26
+
27
+ ### 1. Install via Gem
28
+ ```bash
29
+ gem install eksa-framework
30
+ ```
31
+
32
+ ### 2. Inisialisasi Project Baru
33
+ ```bash
34
+ mkdir my-app && cd my-app
35
+ eksa init
36
+ ```
37
+
38
+ ### 3. Jalankan Server
39
+ ```bash
40
+ bundle install
41
+ eksa run
42
+ ```
43
+
44
+ ---
45
+
46
+ ## ๐Ÿ’ป Panduan Pengembangan
47
+
48
+ ### 1. Konfigurasi Aplikasi (`config.ru`)
49
+ Eksa kini menggunakan blok inisialisasi untuk konfigurasi yang lebih fleksibel:
50
+
51
+ ```ruby
52
+ app = Eksa::Application.new do |config|
53
+ config.config[:db_path] = "./db/production.db"
54
+
55
+ config.use Rack::Static, urls: ["/css", "/img"], root: "public"
56
+ config.use Rack::ShowExceptions
57
+ end
58
+ ```
59
+
60
+ ### 2. CLI Generator
61
+ Hemat waktu dengan menggunakan generator bawaan:
62
+
63
+ ```bash
64
+ # Membuat controller dan view template
65
+ eksa g controller Blog
66
+
67
+ # Membuat model dan schema database
68
+ eksa g model Post
69
+
70
+ # Membuat postingan blog baru
71
+ eksa g post "Judul Artikel"
72
+ ```
73
+
74
+ ### 3. Markdown Blog Engine
75
+ Eksa memiliki sistem blog bawaan yang cara kerjanya mirip Jekyll. Cukup buat file `.md` di folder `_posts/` dengan metadata YAML (Front Matter):
76
+
77
+ ```markdown
78
+ ---
79
+ title: "Halo Eksa"
80
+ date: 2026-03-15 14:00:00
81
+ ---
82
+
83
+ Isi konten blog menggunakan **Markdown**.
84
+ ```
85
+
86
+ Fitur Blog:
87
+ * **Dynamic Slug**: Otomatis mengenali rute `/posts/:slug`.
88
+ * **Syntax Highlighting**: Kode di dalam blog otomatis berwarna & punya tombol copy.
89
+ * **Aesthetic UI**: Template blog bawaan dengan desain Glassmorphism.
90
+
91
+ ### 4. Database & Model
92
+ Definisikan schema tabel Anda langsung di dalam model:
93
+
94
+ ```ruby
95
+ class Post < Eksa::Model
96
+ def self.setup_schema
97
+ db.execute <<~SQL
98
+ CREATE TABLE IF NOT EXISTS posts (
99
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
100
+ title TEXT,
101
+ content TEXT
102
+ )
103
+ SQL
104
+ end
105
+ end
106
+ ```
107
+
108
+ ### 4. Asset Helpers
109
+ Gunakan helper di dalam view untuk menyisipkan asset:
110
+
111
+ ```erb
112
+ <%= stylesheet_tag "style" %>
113
+ <%= javascript_tag "app" %>
114
+ ```
115
+
116
+ ### 5. Menjalankan Test
117
+ Pastikan aplikasi Anda berjalan dengan benar menggunakan RSpec:
118
+
119
+ ```bash
120
+ bundle exec rspec
121
+ ```
122
+
123
+ ---
124
+
125
+ ## ๐Ÿ“œ Lisensi
126
+ Proyek ini dilisensikan di bawah **MIT License**. Lihat file [LICENSE](https://github.com/IshikawaUta/eksa-framework/blob/8c8e9046cbce77bbeaeaf673b018eaf0c6db2bbc/LICENSE) untuk detail lebih lanjut.
@@ -0,0 +1,15 @@
1
+ class PostsController < Eksa::Controller
2
+ def index
3
+ @posts = Eksa::MarkdownPost.all
4
+ render "posts/index"
5
+ end
6
+
7
+ def show
8
+ @post = Eksa::MarkdownPost.find(params['slug'])
9
+ if @post
10
+ render "posts/show"
11
+ else
12
+ [404, { 'content-type' => 'text/html' }, ["Post not found"]]
13
+ end
14
+ end
15
+ end
@@ -1,27 +1,40 @@
1
1
  class SeoController < Eksa::Controller
2
2
  def robots
3
+ scheme = request.env['rack.url_scheme'] || 'https'
3
4
  content = <<~TEXT
4
5
  User-agent: *
5
6
  Allow: /
6
7
  Disallow: /hapus
7
8
  Disallow: /edit
8
9
 
9
- Sitemap: https://#{request.host}/sitemap.xml
10
+ Sitemap: #{scheme}://#{request.host}/sitemap.xml
10
11
  TEXT
11
12
  [200, { "Content-Type" => "text/plain" }, [content]]
12
13
  end
13
14
 
14
15
  def sitemap
16
+ scheme = request.env['rack.url_scheme'] || 'https'
17
+ base_url = "#{scheme}://#{request.host}"
15
18
  lastmod = Time.now.strftime("%Y-%m-%d")
16
19
 
17
20
  xml = '<?xml version="1.0" encoding="UTF-8"?>'
18
21
  xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
19
22
 
20
- ["/", "/about", "/docs", "/kontak"].each do |path|
23
+ # Base Pages
24
+ ["/", "/about", "/docs", "/kontak", "/posts"].each do |path|
21
25
  xml += "<url>"
22
- xml += "<loc>https://#{request.host}#{path}</loc>"
26
+ xml += "<loc>#{base_url}#{path}</loc>"
23
27
  xml += "<lastmod>#{lastmod}</lastmod>"
24
- xml += "<priority>0.8</priority>"
28
+ xml += "<priority>#{path == '/' ? '1.0' : '0.8'}</priority>"
29
+ xml += "</url>"
30
+ end
31
+
32
+ # Blog Posts
33
+ Eksa::MarkdownPost.all.each do |post|
34
+ xml += "<url>"
35
+ xml += "<loc>#{base_url}/posts/#{post.slug}</loc>"
36
+ xml += "<lastmod>#{post.date.is_a?(Time) ? post.date.strftime("%Y-%m-%d") : lastmod}</lastmod>"
37
+ xml += "<priority>0.6</priority>"
25
38
  xml += "</url>"
26
39
  end
27
40
 
@@ -29,6 +29,7 @@
29
29
  <ul class="space-y-2 text-sm">
30
30
  <li><a href="#database" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="database" class="w-3 h-3"></i> Auto-Migration</a></li>
31
31
  <li><a href="#flash" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="bell" class="w-3 h-3"></i> Flash UI</a></li>
32
+ <li><a href="#blog" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="pen-tool" class="w-3 h-3"></i> Markdown Blog</a></li>
32
33
  </ul>
33
34
  </div>
34
35
  </div>
@@ -78,6 +79,10 @@
78
79
  <code class="text-indigo-300 font-bold">spec/</code>
79
80
  <p class="text-xs text-white/50 mt-1">Folder testing otomatis menggunakan RSpec (disertakan saat <code class="text-indigo-200">init</code>).</p>
80
81
  </div>
82
+ <div class="bg-white/5 p-4 rounded-xl border border-white/10">
83
+ <code class="text-indigo-300 font-bold">_posts/</code>
84
+ <p class="text-xs text-white/50 mt-1">Pusat file markdown untuk konten blog aplikasi.</p>
85
+ </div>
81
86
  <div class="bg-white/5 p-4 rounded-xl border border-white/10">
82
87
  <code class="text-indigo-300 font-bold">exe/</code>
83
88
  <p class="text-xs text-white/50 mt-1">Executable files seperti CLI <code class="text-indigo-200">eksa</code>.</p>
@@ -125,6 +130,23 @@
125
130
  </div>
126
131
  </section>
127
132
 
133
+ <section id="blog" class="mb-12 scroll-mt-24">
134
+ <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
135
+ <i data-lucide="pen-tool" class="w-6 h-6 text-indigo-400"></i> Markdown Blog Engine
136
+ </h2>
137
+ <p class="text-white/60 mb-4 text-sm">Kelola konten blog dengan file Markdown. Mendukung Front Matter YAML dan rute dinamis.</p>
138
+ <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
139
+ <p class="text-white/30"># Buat post baru lewat CLI</p>
140
+ <p class="text-indigo-300">eksa g post "Judul Postingan"</p>
141
+ <p class="text-white/30 mt-4"># Akses di URL (Otomatis)</p>
142
+ <p class="text-white/70">/posts/:slug</p>
143
+ </div>
144
+ <div class="mt-4 p-4 bg-indigo-500/10 border border-indigo-500/20 rounded-xl flex items-center gap-3">
145
+ <i data-lucide="zap" class="w-5 h-5 text-indigo-400"></i>
146
+ <p class="text-xs text-indigo-200/80">Sudah terintegrasi dengan Prism.js untuk highlighting kode otomatis dan tombol copy!</p>
147
+ </div>
148
+ </section>
149
+
128
150
  <section id="build" class="mb-12 scroll-mt-24">
129
151
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
130
152
  <i data-lucide="package-check" class="w-6 h-6 text-indigo-400"></i> Build & Publish
@@ -133,7 +155,7 @@
133
155
  <p class="text-white/30"># Build gem lokal</p>
134
156
  <p class="text-indigo-300">gem build eksa-framework.gemspec</p>
135
157
  <p class="text-white/30 mt-4"># Publish ke RubyGems / GitHub Packages</p>
136
- <p class="text-indigo-300">gem push eksa-framework-2.2.2.gem</p>
158
+ <p class="text-indigo-300">gem push eksa-framework-3.3.2.gem</p>
137
159
  </div>
138
160
  </section>
139
161
 
@@ -142,7 +164,7 @@
142
164
  <span class="bg-indigo-500/20 text-indigo-300 text-[10px] px-3 py-1 rounded-full border border-indigo-500/30 uppercase tracking-tighter">Rack 3.0 Compatible</span>
143
165
  <span class="bg-emerald-500/20 text-emerald-300 text-[10px] px-3 py-1 rounded-full border border-emerald-500/30 uppercase tracking-tighter">SQLite3 Auto-Ready</span>
144
166
  </div>
145
- <p class="text-white/40 text-sm italic">Eksa Framework v<%= Eksa::VERSION %> Alpha Documentation</p>
167
+ <p class="text-white/40 text-sm italic">Eksa Framework v3.3.2 Alpha Documentation</p>
146
168
  <a href="/" class="inline-block mt-6 text-indigo-300 hover:text-white transition font-bold flex items-center justify-center gap-2">
147
169
  <i data-lucide="arrow-left" class="w-4 h-4"></i> Kembali ke Beranda
148
170
  </a>
@@ -2,7 +2,7 @@
2
2
  <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
3
3
  <div>
4
4
  <span class="inline-block bg-indigo-500/20 text-indigo-200 text-xs font-bold px-3 py-1 rounded-full mb-3 border border-indigo-500/30">
5
- v2.2.2 Alpha
5
+ v3.3.2 Alpha
6
6
  </span>
7
7
  <h1 class="text-4xl font-extrabold tracking-tight">
8
8
  Halo, <span class="text-indigo-300"><%= @nama %></span>!
@@ -10,13 +10,80 @@
10
10
  <meta property="og:title" content="Eksa Framework">
11
11
  <meta property="og:description" content="Membangun web cepat dengan estetika transparan.">
12
12
  <meta property="og:type" content="website">
13
+
14
+ <%
15
+ scheme = request.env['rack.url_scheme'] || 'https'
16
+ base_url = "#{scheme}://#{request.host}"
17
+ current_url = "#{base_url}#{request.path}"
18
+
19
+ if @post
20
+ structured_data = {
21
+ "@context": "https://schema.org",
22
+ "@type": "BlogPosting",
23
+ "headline": @post.title,
24
+ "datePublished": (@post.date.is_a?(Time) ? @post.date.iso8601 : @post.date),
25
+ "author": {
26
+ "@type": "Person",
27
+ "name": "IshikawaUta"
28
+ },
29
+ "url": current_url,
30
+ "description": @post.body_html[0..160].gsub(/<[^>]*>/, '').strip + "..."
31
+ }
32
+ else
33
+ structured_data = {
34
+ "@context": "https://schema.org",
35
+ "@type": "WebSite",
36
+ "name": "Eksa Framework",
37
+ "url": base_url,
38
+ "description": "Framework Ruby Modern dengan sentuhan Glassmorphism"
39
+ }
40
+ end
41
+ %>
42
+ <script type="application/ld+json">
43
+ <%= structured_data.to_json %>
44
+ </script>
13
45
  <link rel="preconnect" href="https://fonts.googleapis.com">
14
46
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
47
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
16
- <script src="https://cdn.tailwindcss.com"></script>
48
+ <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
17
49
  <link rel="icon" href="/img/logo.png" type="image/png">
18
50
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
19
51
  <script src="https://unpkg.com/lucide@latest"></script>
52
+
53
+ <!-- Prism.js Syntax Highlighting -->
54
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css">
55
+ <style>
56
+ pre[class*="language-"] {
57
+ border-radius: 1.5rem !important;
58
+ background: rgba(0, 0, 0, 0.4) !important;
59
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
60
+ backdrop-filter: blur(5px);
61
+ margin: 2rem 0 !important;
62
+ padding: 1.5rem !important;
63
+ }
64
+ .code-wrapper { position: relative; }
65
+ .copy-btn {
66
+ position: absolute;
67
+ top: 0.75rem;
68
+ right: 0.75rem;
69
+ background: rgba(255, 255, 255, 0.1);
70
+ backdrop-filter: blur(10px);
71
+ border: 1px solid rgba(255, 255, 255, 0.1);
72
+ padding: 0.5rem;
73
+ border-radius: 0.75rem;
74
+ color: rgba(255, 255, 255, 0.5);
75
+ cursor: pointer;
76
+ transition: all 0.2s;
77
+ z-index: 10;
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 0.5rem;
81
+ font-size: 0.75rem;
82
+ font-weight: 600;
83
+ }
84
+ .copy-btn:hover { background: rgba(255, 255, 255, 0.2); color: white; }
85
+ .copy-btn.copied { background: #10b981; color: white; border-color: #34d399; }
86
+ </style>
20
87
 
21
88
  <style>
22
89
  body {
@@ -121,6 +188,7 @@
121
188
  <li><a href="/" class="nav-link flex items-center gap-2"><i data-lucide="home" class="w-4 h-4"></i> Home</a></li>
122
189
  <li><a href="/about" class="nav-link flex items-center gap-2"><i data-lucide="info" class="w-4 h-4"></i> About</a></li>
123
190
  <li><a href="/docs" class="nav-link flex items-center gap-2"><i data-lucide="book-open" class="w-4 h-4"></i> Docs</a></li>
191
+ <li><a href="/posts" class="nav-link flex items-center gap-2"><i data-lucide="book" class="w-4 h-4"></i> Blog</a></li>
124
192
  <li><a href="/kontak" class="nav-link flex items-center gap-2"><i data-lucide="message-circle" class="w-4 h-4"></i> Contact</a></li>
125
193
  </ul>
126
194
 
@@ -137,9 +205,45 @@
137
205
  <p>&copy; <%= Time.now.year %> <span class="font-bold text-white/60">Eksa Framework</span>. Crafted for speed.</p>
138
206
  </footer>
139
207
 
208
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
209
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
140
210
  <script>
141
211
  // Inisialisasi ikon Lucide
142
212
  lucide.createIcons();
213
+
214
+ // Setup Copy Buttons for Code Blocks
215
+ document.querySelectorAll('pre').forEach((block) => {
216
+ // Wrap pre in a container
217
+ const wrapper = document.createElement('div');
218
+ wrapper.className = 'code-wrapper';
219
+ block.parentNode.insertBefore(wrapper, block);
220
+ wrapper.appendChild(block);
221
+
222
+ // Create button
223
+ const button = document.createElement('button');
224
+ button.className = 'copy-btn';
225
+ button.innerHTML = '<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy';
226
+ wrapper.appendChild(button);
227
+
228
+ button.addEventListener('click', async () => {
229
+ const text = block.textContent;
230
+ try {
231
+ await navigator.clipboard.writeText(text);
232
+ button.classList.add('copied');
233
+ button.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5"></i> Copied!';
234
+ lucide.createIcons();
235
+
236
+ setTimeout(() => {
237
+ button.classList.remove('copied');
238
+ button.innerHTML = '<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy';
239
+ lucide.createIcons();
240
+ }, 2000);
241
+ } catch (err) {
242
+ console.error('Failed to copy!', err);
243
+ }
244
+ });
245
+ });
246
+ lucide.createIcons();
143
247
  </script>
144
248
  </body>
145
249
  </html>
@@ -0,0 +1,43 @@
1
+ <div class="glass rounded-3xl p-10 text-white animate__animated animate__fadeIn">
2
+ <div class="flex items-center justify-between mb-8">
3
+ <div class="flex items-center gap-4">
4
+ <div class="p-4 bg-indigo-500 rounded-2xl shadow-xl">
5
+ <i data-lucide="book-open" class="w-8 h-8"></i>
6
+ </div>
7
+ <div>
8
+ <h1 class="text-4xl font-extrabold tracking-tight">Blog <span class="text-indigo-300">Markdown</span></h1>
9
+ <p class="text-white/50">Dikelola langsung dari folder _posts</p>
10
+ </div>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
15
+ <% if @posts.empty? %>
16
+ <div class="col-span-full text-center py-20 bg-white/5 rounded-3xl border border-dashed border-white/20">
17
+ <i data-lucide="ghost" class="w-12 h-12 text-white/20 mx-auto mb-4"></i>
18
+ <p class="text-white/40">Belum ada postingan blog. Coba jalankan <code>eksa g post "Judul Pertama"</code></p>
19
+ </div>
20
+ <% else %>
21
+ <% @posts.each do |post| %>
22
+ <a href="/posts/<%= post.slug %>" class="group block bg-white/5 hover:bg-white/10 p-6 rounded-3xl border border-white/10 transition-all hover:scale-[1.02]">
23
+ <span class="text-xs font-bold text-indigo-300 uppercase tracking-widest block mb-2">
24
+ <%= post.date.is_a?(Time) ? post.date.strftime("%d %B %Y") : post.date %>
25
+ </span>
26
+ <h2 class="text-2xl font-bold mb-3 group-hover:text-indigo-300 transition-colors"><%= post.title %></h2>
27
+ <div class="flex items-center gap-2 text-white/40 text-sm">
28
+ <span>Baca selengkapnya</span>
29
+ <i data-lucide="arrow-right" class="w-4 h-4 group-hover:translate-x-1 transition-transform"></i>
30
+ </div>
31
+ </a>
32
+ <% end %>
33
+ <% end %>
34
+ </div>
35
+
36
+ <div class="mt-12 pt-8 border-t border-white/10">
37
+ <a href="/" class="flex items-center gap-2 text-indigo-300 hover:text-white transition">
38
+ <i data-lucide="arrow-left" class="w-4 h-4"></i> Kembali ke Home
39
+ </a>
40
+ </div>
41
+ </div>
42
+
43
+ <script>lucide.createIcons();</script>
@@ -0,0 +1,31 @@
1
+ <div class="glass rounded-3xl p-10 text-white animate__animated animate__fadeIn">
2
+ <div class="mb-8">
3
+ <a href="/posts" class="inline-flex items-center gap-2 text-indigo-300 hover:text-white transition mb-6">
4
+ <i data-lucide="arrow-left" class="w-4 h-4"></i> Kembali ke Blog
5
+ </a>
6
+ <span class="text-xs font-bold text-indigo-300 uppercase tracking-widest block mb-2">
7
+ <%= @post.date.is_a?(Time) ? @post.date.strftime("%d %B %Y") : @post.date %>
8
+ </span>
9
+ <h3 class="text-5xl font-extrabold tracking-tight mb-4"><%= @post.title %></h3>
10
+ <div class="h-1 w-20 bg-indigo-500 rounded-full"></div>
11
+ </div>
12
+
13
+ <div class="prose prose-invert prose-indigo max-w-none leading-relaxed text-white/80">
14
+ <%= @post.body_html %>
15
+ </div>
16
+
17
+ <style>
18
+ .prose h1, .prose h2, .prose h3 { color: white; margin-top: 2em; margin-bottom: 0.5em; font-weight: 800; }
19
+ .prose h2 { font-size: 1.8em; }
20
+ .prose p { margin-bottom: 1.5em; }
21
+ .prose blockquote { border-left: 4px solid #6366f1; padding-left: 1.5em; font-style: italic; color: rgba(255,255,255,0.6); }
22
+ .prose ul { list-style-type: disc; padding-left: 1.5em; margin-bottom: 1.5em; }
23
+ .prose code { background: rgba(255,255,255,0.1); padding: 0.2em 0.4em; border-radius: 0.4em; font-size: 0.9em; font-weight: 600; color: #a5b4fc; }
24
+ .prose code::before, .prose code::after { content: none !important; }
25
+ .prose pre code { background: transparent; padding: 0; color: inherit; font-weight: inherit; }
26
+ /* Prism tweaks */
27
+ code[class*="language-"], pre[class*="language-"] { font-size: 0.9em !important; text-shadow: none !important; }
28
+ </style>
29
+ </div>
30
+
31
+ <script>lucide.createIcons();</script>
data/config.ru CHANGED
@@ -17,5 +17,7 @@ app.add_route "/docs", PagesController, :docs
17
17
  app.add_route "/kontak", PagesController, :kontak
18
18
  app.add_route "/robots.txt", SeoController, :robots
19
19
  app.add_route "/sitemap.xml", SeoController, :sitemap
20
+ app.add_route "/posts", PostsController, :index
21
+ app.add_route "/posts/:slug", PostsController, :show
20
22
 
21
23
  run app
data/exe/eksa CHANGED
@@ -10,6 +10,7 @@ def usage
10
10
  puts " eksa init - Inisialisasi project baru"
11
11
  puts " eksa g controller NAME - Generate controller baru"
12
12
  puts " eksa g model NAME - Generate model baru"
13
+ puts " eksa g post TITLE - Generate blog post markdown baru"
13
14
  puts " eksa run - Jalankan server aplikasi"
14
15
  puts "-----------------------------\n"
15
16
  end
@@ -20,7 +21,7 @@ def init_project
20
21
 
21
22
  puts "๐Ÿš€ Menginisialisasi project di: #{target_dir}"
22
23
 
23
- items_to_init = ['app', 'lib', 'public', 'db', 'spec', 'config.ru', 'Gemfile', 'README.md']
24
+ items_to_init = ['app', 'lib', 'public', 'db', 'spec', '_posts', 'config.ru', 'Gemfile', 'README.md']
24
25
 
25
26
  items_to_init.each do |item|
26
27
  source = File.join(gem_root, item)
@@ -114,6 +115,26 @@ def generate_model(name)
114
115
  puts " [OK] Created #{file_path}"
115
116
  end
116
117
 
118
+ def generate_post(title)
119
+ FileUtils.mkdir_p("_posts")
120
+ date = Time.now.strftime("%Y-%m-%d")
121
+ slug = title.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
122
+ filename = "_posts/#{date}-#{slug}.md"
123
+
124
+ content = <<~MARKDOWN
125
+ ---
126
+ title: "#{title}"
127
+ date: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}
128
+ ---
129
+
130
+ Tulis konten blog Anda di sini...
131
+ MARKDOWN
132
+
133
+ File.write(filename, content)
134
+ puts "๐Ÿ› ๏ธ Generating post: #{title}"
135
+ puts " [OK] Created #{filename}"
136
+ end
137
+
117
138
  def start_server
118
139
  if File.exist?('config.ru')
119
140
  puts "๐Ÿš€ Memulai Eksa Framework Server..."
@@ -138,6 +159,8 @@ when 'g', 'generate'
138
159
  generate_controller(name)
139
160
  elsif subcommand == 'model' && name
140
161
  generate_model(name)
162
+ elsif subcommand == 'post' && name
163
+ generate_post(name)
141
164
  else
142
165
  usage
143
166
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+
4
+ require 'yaml'
5
+ require 'kramdown'
6
+
7
+ module Eksa
8
+ class MarkdownPost
9
+ attr_reader :content, :metadata, :slug, :filename
10
+
11
+ POSTS_DIR = "_posts"
12
+
13
+ def self.all
14
+ return [] unless Dir.exist?(POSTS_DIR)
15
+
16
+ Dir.glob(File.join(POSTS_DIR, "*.md")).map do |file|
17
+ new(file)
18
+ end.sort_by { |p| p.date }.reverse
19
+ end
20
+
21
+ def self.find(slug)
22
+ all.find { |p| p.slug == slug }
23
+ end
24
+
25
+ def initialize(file_path)
26
+ @filename = File.basename(file_path)
27
+ @slug = @filename.sub(/\.md$/, '')
28
+ load_file(file_path)
29
+ end
30
+
31
+ def title
32
+ @metadata['title'] || "Untitled Post"
33
+ end
34
+
35
+ def date
36
+ @metadata['date'] || File.mtime(File.join(POSTS_DIR, @filename))
37
+ end
38
+
39
+ def body_html
40
+ Kramdown::Document.new(@content, input: 'GFM').to_html
41
+ end
42
+
43
+ private
44
+
45
+ def load_file(file_path)
46
+ content = File.read(file_path, encoding: 'utf-8')
47
+ # Improved regex: handle optional trailing newline after second separator
48
+ if content =~ /\A(---\s*\r?\n.*?\r?\n)^(---\s*\r?\n?)/m
49
+ @metadata = YAML.safe_load($1, permitted_classes: [Time])
50
+ @content = $'
51
+ else
52
+ @metadata = {}
53
+ @content = content
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/eksa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eksa
2
- VERSION = "2.2.2"
2
+ VERSION = "3.3.2"
3
3
  end
data/lib/eksa.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'rack'
2
+ require 'json'
2
3
  require_relative 'eksa/version'
3
4
  require_relative 'eksa/controller'
4
5
  require_relative 'eksa/model'
6
+ require_relative 'eksa/markdown_post'
5
7
 
6
8
  module Eksa
7
9
  class Application
@@ -22,7 +24,13 @@ module Eksa
22
24
  end
23
25
 
24
26
  def add_route(path, controller_class, action)
25
- @routes[path] = { controller: controller_class, action: action }
27
+ pattern = path.gsub(/:\w+/, '([^/]+)')
28
+ @routes[path] = {
29
+ controller: controller_class,
30
+ action: action,
31
+ regex: Regexp.new("\\A#{pattern}\\z"),
32
+ keys: path.scan(/:(\w+)/).flatten
33
+ }
26
34
  end
27
35
 
28
36
  def use(middleware, *args, &block)
@@ -46,12 +54,28 @@ module Eksa
46
54
  def core_call(env)
47
55
  request = Rack::Request.new(env)
48
56
  flash_message = request.cookies['eksa_flash']
49
- route = @routes[request.path_info]
57
+
58
+ # Find matching route
59
+ route_config = nil
60
+ params = {}
61
+
62
+ @routes.each do |path, config|
63
+ if match = config[:regex].match(request.path_info)
64
+ route_config = config
65
+ config[:keys].each_with_index do |key, index|
66
+ params[key] = match[index + 1]
67
+ end
68
+ break
69
+ end
70
+ end
50
71
 
51
- if route
52
- controller_instance = route[:controller].new(request)
72
+ if route_config
73
+ # Merge dynamic params into request params
74
+ request.params.merge!(params)
75
+
76
+ controller_instance = route_config[:controller].new(request)
53
77
  controller_instance.flash[:notice] = flash_message if flash_message
54
- response_data = controller_instance.send(route[:action])
78
+ response_data = controller_instance.send(route_config[:action])
55
79
  if response_data.is_a?(Array) && response_data.size == 3
56
80
  status, headers, body = response_data
57
81
  response = Rack::Response.new(body, status, headers)
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ describe Eksa::MarkdownPost do
5
+ before(:all) do
6
+ FileUtils.mkdir_p("_posts")
7
+ File.write("_posts/2026-03-15-test-post.md", <<~MARKDOWN)
8
+ ---
9
+ title: "Test Post"
10
+ date: 2026-03-15 10:00:00
11
+ ---
12
+ # Hello World
13
+ This is a **test**.
14
+ MARKDOWN
15
+ end
16
+
17
+ after(:all) do
18
+ File.delete("_posts/2026-03-15-test-post.md") if File.exist?("_posts/2026-03-15-test-post.md")
19
+ end
20
+
21
+ it "loads metadata correctly" do
22
+ post = Eksa::MarkdownPost.find("2026-03-15-test-post")
23
+ expect(post.title).to eq("Test Post")
24
+ end
25
+
26
+ it "converts markdown to html" do
27
+ post = Eksa::MarkdownPost.find("2026-03-15-test-post")
28
+ expect(post.body_html).to include("<h1 id=\"hello-world\">Hello World</h1>")
29
+ expect(post.body_html).to include("<strong>test</strong>")
30
+ end
31
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eksa-framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.2
4
+ version: 3.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - IshikawaUta
@@ -65,6 +65,34 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '2.3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: kramdown
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.4'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.4'
82
+ - !ruby/object:Gem::Dependency
83
+ name: kramdown-parser-gfm
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.1'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.1'
68
96
  description: Framework MVC ringan dengan tema modern, sistem routing, dan auto-database
69
97
  SQLite.
70
98
  email:
@@ -78,7 +106,9 @@ files:
78
106
  - Gemfile.lock
79
107
  - LICENSE
80
108
  - README.md
109
+ - _posts/2026-03-15-welcome-to-eksa-framework.md
81
110
  - app/controllers/pages_controller.rb
111
+ - app/controllers/posts_controller.rb
82
112
  - app/controllers/seo_controller.rb
83
113
  - app/models/pesan.rb
84
114
  - app/views/about.html.erb
@@ -87,17 +117,21 @@ files:
87
117
  - app/views/index.html.erb
88
118
  - app/views/kontak.html.erb
89
119
  - app/views/layout.html.erb
120
+ - app/views/posts/index.html.erb
121
+ - app/views/posts/show.html.erb
90
122
  - config.ru
91
123
  - db/setup.rb
92
124
  - exe/eksa
93
125
  - lib/eksa.rb
94
126
  - lib/eksa/controller.rb
127
+ - lib/eksa/markdown_post.rb
95
128
  - lib/eksa/model.rb
96
129
  - lib/eksa/version.rb
97
130
  - public/css/style.css
98
131
  - public/img/favicon.ico
99
132
  - public/img/logo.png
100
133
  - spec/application_spec.rb
134
+ - spec/markdown_post_spec.rb
101
135
  - spec/model_spec.rb
102
136
  - spec/spec_helper.rb
103
137
  homepage: https://github.com/IshikawaUta/eksa-framework