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 +4 -4
- data/Gemfile +3 -1
- data/Gemfile.lock +10 -0
- data/README.md +21 -1
- data/_posts/2026-03-15-welcome-to-eksa-framework.md +126 -0
- data/app/controllers/posts_controller.rb +15 -0
- data/app/views/docs.html.erb +24 -2
- data/app/views/index.html.erb +1 -1
- data/app/views/layout.html.erb +73 -1
- data/app/views/posts/index.html.erb +43 -0
- data/app/views/posts/show.html.erb +31 -0
- data/config.ru +2 -0
- data/exe/eksa +24 -1
- data/lib/eksa/markdown_post.rb +54 -0
- data/lib/eksa/version.rb +1 -1
- data/lib/eksa.rb +28 -5
- data/spec/markdown_post_spec.rb +31 -0
- metadata +35 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f15480cfb455196634bec205c45a10a83abf031d398915f38652a793f9b98c84
|
|
4
|
+
data.tar.gz: 33577ecbf0baf8f1bfb66abfbbf30c39e7cfac37500aefb64d2cabe4d50b2e83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61cc9369d8553779a045858906a451e4370a7121370205a2b2ff1311412f16103b76896b9a206cd629150abe1313ee822a582b8e225d13d6c0bcf2be29ffd942
|
|
7
|
+
data.tar.gz: d05d48c0ee928aa8c70942224a003632ac29404e5a588e377724570a51ca1afdb6c3bb83dd5489f0e06287f577c1d39c1cc53ada3f6d5303faa741552dc24a42
|
data/Gemfile
CHANGED
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
|
-
|
|
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
|
data/app/views/docs.html.erb
CHANGED
|
@@ -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.
|
|
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
|
|
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>
|
data/app/views/index.html.erb
CHANGED
|
@@ -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.
|
|
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>!
|
data/app/views/layout.html.erb
CHANGED
|
@@ -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>© <%= 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
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
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
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(
|
|
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.
|
|
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
|