eksa-framework 2.2.2 → 2.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: f15480cfb455196634bec205c45a10a83abf031d398915f38652a793f9b98c84
4
+ data.tar.gz: 33577ecbf0baf8f1bfb66abfbbf30c39e7cfac37500aefb64d2cabe4d50b2e83
5
5
  SHA512:
6
- metadata.gz: 12e84b512d1b40e62b47dcd10fa06334a2ae49103e251bfa4f1d36df607193b5719563aed61d25a84c252dd8c2cfb55aebcd0868185b1968dcfbdc46c6b6a2f3
7
- data.tar.gz: 9704f8011116a0d726685e35f8540e1bd592d29e35db3c3ddcaab03b960fc474c52ef46e44ee07b5757ee539a044e8beba3290eba06874d5821db7b90b4c901a
6
+ metadata.gz: 61cc9369d8553779a045858906a451e4370a7121370205a2b2ff1311412f16103b76896b9a206cd629150abe1313ee822a582b8e225d13d6c0bcf2be29ffd942
7
+ data.tar.gz: d05d48c0ee928aa8c70942224a003632ac29404e5a588e377724570a51ca1afdb6c3bb83dd5489f0e06287f577c1d39c1cc53ada3f6d5303faa741552dc24a42
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
@@ -66,9 +66,29 @@ eksa g controller Blog
66
66
 
67
67
  # Membuat model dan schema database
68
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**.
69
84
  ```
70
85
 
71
- ### 3. Database & Model
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
72
92
  Definisikan schema tabel Anda langsung di dalam model:
73
93
 
74
94
  ```ruby
@@ -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
@@ -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-2.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 v2.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
+ v2.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>!
@@ -13,10 +13,45 @@
13
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
14
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
15
  <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>
16
+ <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
17
17
  <link rel="icon" href="/img/logo.png" type="image/png">
18
18
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
19
19
  <script src="https://unpkg.com/lucide@latest"></script>
20
+
21
+ <!-- Prism.js Syntax Highlighting -->
22
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css">
23
+ <style>
24
+ pre[class*="language-"] {
25
+ border-radius: 1.5rem !important;
26
+ background: rgba(0, 0, 0, 0.4) !important;
27
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
28
+ backdrop-filter: blur(5px);
29
+ margin: 2rem 0 !important;
30
+ padding: 1.5rem !important;
31
+ }
32
+ .code-wrapper { position: relative; }
33
+ .copy-btn {
34
+ position: absolute;
35
+ top: 0.75rem;
36
+ right: 0.75rem;
37
+ background: rgba(255, 255, 255, 0.1);
38
+ backdrop-filter: blur(10px);
39
+ border: 1px solid rgba(255, 255, 255, 0.1);
40
+ padding: 0.5rem;
41
+ border-radius: 0.75rem;
42
+ color: rgba(255, 255, 255, 0.5);
43
+ cursor: pointer;
44
+ transition: all 0.2s;
45
+ z-index: 10;
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 0.5rem;
49
+ font-size: 0.75rem;
50
+ font-weight: 600;
51
+ }
52
+ .copy-btn:hover { background: rgba(255, 255, 255, 0.2); color: white; }
53
+ .copy-btn.copied { background: #10b981; color: white; border-color: #34d399; }
54
+ </style>
20
55
 
21
56
  <style>
22
57
  body {
@@ -121,6 +156,7 @@
121
156
  <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
157
  <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
158
  <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>
159
+ <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
160
  <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
161
  </ul>
126
162
 
@@ -137,9 +173,45 @@
137
173
  <p>&copy; <%= Time.now.year %> <span class="font-bold text-white/60">Eksa Framework</span>. Crafted for speed.</p>
138
174
  </footer>
139
175
 
176
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
177
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
140
178
  <script>
141
179
  // Inisialisasi ikon Lucide
142
180
  lucide.createIcons();
181
+
182
+ // Setup Copy Buttons for Code Blocks
183
+ document.querySelectorAll('pre').forEach((block) => {
184
+ // Wrap pre in a container
185
+ const wrapper = document.createElement('div');
186
+ wrapper.className = 'code-wrapper';
187
+ block.parentNode.insertBefore(wrapper, block);
188
+ wrapper.appendChild(block);
189
+
190
+ // Create button
191
+ const button = document.createElement('button');
192
+ button.className = 'copy-btn';
193
+ button.innerHTML = '<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy';
194
+ wrapper.appendChild(button);
195
+
196
+ button.addEventListener('click', async () => {
197
+ const text = block.textContent;
198
+ try {
199
+ await navigator.clipboard.writeText(text);
200
+ button.classList.add('copied');
201
+ button.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5"></i> Copied!';
202
+ lucide.createIcons();
203
+
204
+ setTimeout(() => {
205
+ button.classList.remove('copied');
206
+ button.innerHTML = '<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy';
207
+ lucide.createIcons();
208
+ }, 2000);
209
+ } catch (err) {
210
+ console.error('Failed to copy!', err);
211
+ }
212
+ });
213
+ });
214
+ lucide.createIcons();
143
215
  </script>
144
216
  </body>
145
217
  </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,54 @@
1
+ require 'yaml'
2
+ require 'kramdown'
3
+
4
+ module Eksa
5
+ class MarkdownPost
6
+ attr_reader :content, :metadata, :slug, :filename
7
+
8
+ POSTS_DIR = "_posts"
9
+
10
+ def self.all
11
+ return [] unless Dir.exist?(POSTS_DIR)
12
+
13
+ Dir.glob(File.join(POSTS_DIR, "*.md")).map do |file|
14
+ new(file)
15
+ end.sort_by { |p| p.date }.reverse
16
+ end
17
+
18
+ def self.find(slug)
19
+ all.find { |p| p.slug == slug }
20
+ end
21
+
22
+ def initialize(file_path)
23
+ @filename = File.basename(file_path)
24
+ @slug = @filename.sub(/\.md$/, '')
25
+ load_file(file_path)
26
+ end
27
+
28
+ def title
29
+ @metadata['title'] || "Untitled Post"
30
+ end
31
+
32
+ def date
33
+ @metadata['date'] || File.mtime(File.join(POSTS_DIR, @filename))
34
+ end
35
+
36
+ def body_html
37
+ Kramdown::Document.new(@content, input: 'GFM').to_html
38
+ end
39
+
40
+ private
41
+
42
+ def load_file(file_path)
43
+ content = File.read(file_path)
44
+ # Improved regex: handle optional trailing newline after second separator
45
+ if content =~ /\A(---\s*\r?\n.*?\r?\n)^(---\s*\r?\n?)/m
46
+ @metadata = YAML.safe_load($1, permitted_classes: [Time])
47
+ @content = $'
48
+ else
49
+ @metadata = {}
50
+ @content = content
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/eksa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eksa
2
- VERSION = "2.2.2"
2
+ VERSION = "2.3.2"
3
3
  end
data/lib/eksa.rb CHANGED
@@ -2,6 +2,7 @@ require 'rack'
2
2
  require_relative 'eksa/version'
3
3
  require_relative 'eksa/controller'
4
4
  require_relative 'eksa/model'
5
+ require_relative 'eksa/markdown_post'
5
6
 
6
7
  module Eksa
7
8
  class Application
@@ -22,7 +23,13 @@ module Eksa
22
23
  end
23
24
 
24
25
  def add_route(path, controller_class, action)
25
- @routes[path] = { controller: controller_class, action: action }
26
+ pattern = path.gsub(/:\w+/, '([^/]+)')
27
+ @routes[path] = {
28
+ controller: controller_class,
29
+ action: action,
30
+ regex: Regexp.new("\\A#{pattern}\\z"),
31
+ keys: path.scan(/:(\w+)/).flatten
32
+ }
26
33
  end
27
34
 
28
35
  def use(middleware, *args, &block)
@@ -46,12 +53,28 @@ module Eksa
46
53
  def core_call(env)
47
54
  request = Rack::Request.new(env)
48
55
  flash_message = request.cookies['eksa_flash']
49
- route = @routes[request.path_info]
56
+
57
+ # Find matching route
58
+ route_config = nil
59
+ params = {}
60
+
61
+ @routes.each do |path, config|
62
+ if match = config[:regex].match(request.path_info)
63
+ route_config = config
64
+ config[:keys].each_with_index do |key, index|
65
+ params[key] = match[index + 1]
66
+ end
67
+ break
68
+ end
69
+ end
50
70
 
51
- if route
52
- controller_instance = route[:controller].new(request)
71
+ if route_config
72
+ # Merge dynamic params into request params
73
+ request.params.merge!(params)
74
+
75
+ controller_instance = route_config[:controller].new(request)
53
76
  controller_instance.flash[:notice] = flash_message if flash_message
54
- response_data = controller_instance.send(route[:action])
77
+ response_data = controller_instance.send(route_config[:action])
55
78
  if response_data.is_a?(Array) && response_data.size == 3
56
79
  status, headers, body = response_data
57
80
  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: 2.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