@bivabdas/sharq 1.0.1 → 1.0.3

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.
package/bin/sharq.js CHANGED
@@ -29,13 +29,15 @@ Usage:
29
29
 
30
30
  Commands:
31
31
  start Start the local sharq server
32
- dev Alias for start, intended for local development
32
+ dev Start the local sharq server with Tailwind watching enabled
33
33
  build Generate a deployable static site in ./sharq
34
34
  create Scaffold a new sharq site
35
35
  `);
36
36
  }
37
37
 
38
- if (!command || command === "dev" || command === "start") {
38
+ if (!command || command === "dev") {
39
+ await startServer({ watchStyles: true });
40
+ } else if (command === "start") {
39
41
  await startServer();
40
42
  } else if (command === "build") {
41
43
  const config = await resolveConfig();
package/lib/config.js CHANGED
@@ -13,6 +13,7 @@ export function createConfig(rootDir, overrides = {}) {
13
13
  overrides.siteDescription || "A tiny publishing surface that renders posts from Markdown and serves static HTML.",
14
14
  buildDirName,
15
15
  contentDir: path.join(rootDir, "content"),
16
+ stylesDir: path.join(rootDir, "styles"),
16
17
  templatesDir: path.join(rootDir, "templates"),
17
18
  publicDir: path.join(rootDir, "public"),
18
19
  blogOutputDir: path.join(rootDir, "public", "blog"),
@@ -20,6 +21,9 @@ export function createConfig(rootDir, overrides = {}) {
20
21
  homePagePath: path.join(rootDir, "public", "index.html"),
21
22
  archivePagePath: path.join(rootDir, "public", "archive.html"),
22
23
  staticAssetsDir: path.join(rootDir, "public", "assets"),
24
+ tailwindInputPath: path.join(rootDir, "styles", "site.css"),
25
+ tailwindOutputPath: path.join(rootDir, "public", "assets", "site.css"),
26
+ overridesCssPath: path.join(rootDir, "public", "assets", "overrides.css"),
23
27
  buildDir: path.join(rootDir, buildDirName)
24
28
  };
25
29
  }
package/lib/generator.js CHANGED
@@ -2,10 +2,12 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { generateSitemap } from "./sitemap.js";
4
4
  import { readPostSource, renderArchivePage, renderIndexPage, renderPostPage } from "./renderer.js";
5
+ import { compileTailwind } from "./styles.js";
5
6
 
6
7
  async function ensureDirectories(config) {
7
8
  await Promise.all([
8
9
  fs.mkdir(config.contentDir, { recursive: true }),
10
+ fs.mkdir(config.stylesDir, { recursive: true }),
9
11
  fs.mkdir(config.publicDir, { recursive: true }),
10
12
  fs.mkdir(config.blogOutputDir, { recursive: true }),
11
13
  fs.mkdir(config.templatesDir, { recursive: true }),
@@ -121,6 +123,7 @@ function createOutputConfig(config, outputDir) {
121
123
  }
122
124
 
123
125
  export async function buildStaticSite(config, outputDir = config.buildDir) {
126
+ await compileTailwind(config, { minify: true });
124
127
  const outputConfig = createOutputConfig(config, outputDir);
125
128
 
126
129
  await fs.rm(outputDir, { recursive: true, force: true });
package/lib/renderer.js CHANGED
@@ -228,7 +228,7 @@ export async function renderPostPage(config, post) {
228
228
  .join("");
229
229
 
230
230
  return applyTemplate(template, {
231
- title: escapeHtml(post.title),
231
+ title: escapeHtml(`${post.title} | ${config.siteTitle}`),
232
232
  description: escapeHtml(post.description),
233
233
  date: escapeHtml(post.date),
234
234
  author: escapeHtml(post.author || "sharq"),
package/lib/scaffold.js CHANGED
@@ -77,6 +77,10 @@ async function writePackageJson(projectDir, projectName, frameworkPackageName, f
77
77
  },
78
78
  dependencies: {
79
79
  [frameworkPackageName]: frameworkDependency
80
+ },
81
+ devDependencies: {
82
+ tailwindcss: "^4.0.0",
83
+ "@tailwindcss/cli": "^4.0.0"
80
84
  }
81
85
  };
82
86
 
@@ -107,8 +111,9 @@ Generated with sharq.
107
111
  ## Structure
108
112
 
109
113
  - \`content/\` markdown posts
114
+ - \`styles/site.css\` Tailwind entry file
110
115
  - \`templates/\` page templates
111
- - \`public/assets/\` static assets
116
+ - \`public/assets/overrides.css\` optional vanilla CSS overrides
112
117
  - \`sharq/\` production build output
113
118
  - \`sharq.config.js\` project-level settings
114
119
  `;
package/lib/server.js CHANGED
@@ -3,6 +3,7 @@ import http from "node:http";
3
3
  import path from "node:path";
4
4
  import { resolveConfig } from "./config.js";
5
5
  import { ensurePostGenerated, refreshSiteArtifacts } from "./generator.js";
6
+ import { compileTailwind } from "./styles.js";
6
7
 
7
8
  const contentTypes = {
8
9
  ".html": "text/html; charset=utf-8",
@@ -186,13 +187,23 @@ export async function startServer(options = {}) {
186
187
  const config = await resolveConfig(rootDir);
187
188
  const requestedPort = Number(options.port || process.env.PORT || 3000);
188
189
  const maxPortAttempts = Number(options.maxPortAttempts || 20);
190
+ const watchStyles = Boolean(options.watchStyles);
189
191
  const app = createApp(config);
192
+
193
+ await compileTailwind(config);
194
+ const styleWatcher = watchStyles ? await compileTailwind(config, { watch: true }) : null;
190
195
  const { server, port } = await listenOnAvailablePort(() => app.createServer(), requestedPort, maxPortAttempts);
191
196
 
192
197
  if (!process.env.SITE_URL) {
193
198
  config.siteUrl = `http://localhost:${port}`;
194
199
  }
195
200
 
201
+ if (styleWatcher) {
202
+ server.on("close", () => {
203
+ styleWatcher.kill();
204
+ });
205
+ }
206
+
196
207
  await app.prepare();
197
208
  console.log(`sharq running at http://localhost:${port}`);
198
209
  return { server, port, config };
package/lib/styles.js ADDED
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs/promises";
2
+ import { spawn } from "node:child_process";
3
+
4
+ function getNpxCommand() {
5
+ return process.platform === "win32" ? "npx.cmd" : "npx";
6
+ }
7
+
8
+ export async function hasTailwindInput(config) {
9
+ try {
10
+ await fs.access(config.tailwindInputPath);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ function runTailwind(config, { watch = false, minify = false } = {}) {
18
+ const args = [
19
+ "@tailwindcss/cli",
20
+ "-i",
21
+ config.tailwindInputPath,
22
+ "-o",
23
+ config.tailwindOutputPath
24
+ ];
25
+
26
+ if (watch) {
27
+ args.push("--watch");
28
+ }
29
+
30
+ if (minify) {
31
+ args.push("--minify");
32
+ }
33
+
34
+ return spawn(getNpxCommand(), args, {
35
+ cwd: config.rootDir,
36
+ stdio: watch ? "inherit" : "pipe"
37
+ });
38
+ }
39
+
40
+ export async function compileTailwind(config, options = {}) {
41
+ if (!(await hasTailwindInput(config))) {
42
+ return false;
43
+ }
44
+
45
+ await fs.mkdir(config.staticAssetsDir, { recursive: true });
46
+
47
+ const child = runTailwind(config, options);
48
+
49
+ if (options.watch) {
50
+ return child;
51
+ }
52
+
53
+ let stderr = "";
54
+
55
+ if (child.stderr) {
56
+ child.stderr.on("data", (chunk) => {
57
+ stderr += chunk.toString();
58
+ });
59
+ }
60
+
61
+ return new Promise((resolve, reject) => {
62
+ child.on("exit", (code) => {
63
+ if (code === 0) {
64
+ resolve(true);
65
+ return;
66
+ }
67
+
68
+ reject(new Error(stderr.trim() || `Tailwind build failed with exit code ${code}.`));
69
+ });
70
+
71
+ child.on("error", (error) => {
72
+ reject(error);
73
+ });
74
+ });
75
+ }
package/package.json CHANGED
@@ -1,36 +1,42 @@
1
1
  {
2
2
  "name": "@bivabdas/sharq",
3
- "version": "1.0.1",
4
- "description": "Minimal on-demand static rendering framework and scaffolder",
5
- "license": "MIT",
6
- "keywords": [
7
- "static-site-generator",
8
- "blog",
9
- "markdown",
10
- "landing-pages",
11
- "publishing"
12
- ],
13
- "engines": {
14
- "node": ">=20.0.0"
15
- },
3
+ "version": "1.0.3",
4
+ "description": "A tiny static publishing framework for landing pages and markdown-driven sites.",
16
5
  "type": "module",
17
- "exports": {
18
- ".": "./index.js"
19
- },
6
+ "main": "index.js",
20
7
  "bin": {
21
8
  "sharq": "./bin/sharq.js",
22
9
  "create-sharq": "./bin/create-sharq.js"
23
10
  },
24
- "scripts": {
25
- "start": "node ./bin/sharq.js start",
26
- "dev": "node --watch ./bin/sharq.js dev",
27
- "build": "node ./bin/sharq.js build",
28
- "test": "node --input-type=module -e \"import('./index.js').then((m) => console.log('exports:', Object.keys(m).sort().join(', ')))\""
29
- },
30
11
  "files": [
31
12
  "bin",
13
+ "index.js",
32
14
  "lib",
33
15
  "scaffold",
34
- "index.js"
35
- ]
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "test": "node --input-type=module -e \"import('./index.js').then((m) => console.log('exports:', Object.keys(m).sort().join(', ')))\""
21
+ },
22
+ "keywords": [
23
+ "static-site-generator",
24
+ "markdown",
25
+ "landing-pages",
26
+ "blog",
27
+ "tailwindcss"
28
+ ],
29
+ "author": "Bivab Das",
30
+ "license": "MIT",
31
+ "homepage": "https://www.sharq.site",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Bivab0/sharq.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/Bivab0/sharq/issues"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ }
36
42
  }
@@ -0,0 +1,38 @@
1
+ body {
2
+ background-color: #f6f4ee;
3
+ background-image:
4
+ linear-gradient(rgba(16, 16, 16, 0.08) 1px, transparent 1px),
5
+ linear-gradient(90deg, rgba(16, 16, 16, 0.08) 1px, transparent 1px),
6
+ radial-gradient(circle at top center, rgba(16, 16, 16, 0.045), transparent 36%);
7
+ background-size: 126px 126px, 126px 126px, auto;
8
+ }
9
+
10
+ .article-content h2,
11
+ .article-content h3 {
12
+ margin-top: 2.2rem;
13
+ }
14
+
15
+ .article-content pre {
16
+ overflow-x: auto;
17
+ border-radius: 1rem;
18
+ background: #121212;
19
+ color: #f6f6f3;
20
+ padding: 1rem 1.125rem;
21
+ }
22
+
23
+ .article-content blockquote {
24
+ margin: 1.5rem 0;
25
+ padding-left: 1rem;
26
+ border-left: 3px solid rgba(16, 16, 16, 0.16);
27
+ }
28
+
29
+ .tag {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ border: 1px solid rgba(16, 16, 16, 0.1);
33
+ border-radius: 999px;
34
+ background: rgba(16, 16, 16, 0.04);
35
+ padding: 0.42rem 0.9rem;
36
+ font-size: 0.86rem;
37
+ color: #66645d;
38
+ }
@@ -1,224 +1,38 @@
1
- :root {
2
- color-scheme: light;
3
- --bg: #efe6d6;
4
- --bg-deep: #ddd3c4;
5
- --panel: rgba(255, 251, 245, 0.9);
6
- --panel-strong: #fff8ef;
7
- --text: #221d18;
8
- --muted: #6d645b;
9
- --accent: #0f766e;
10
- --accent-strong: #9a3412;
11
- --border: rgba(108, 92, 76, 0.16);
12
- --shadow: 0 18px 45px rgba(64, 40, 20, 0.08);
13
- }
14
-
15
- * {
16
- box-sizing: border-box;
17
- }
18
-
19
- html {
20
- font-size: 16px;
21
- }
22
-
23
1
  body {
24
- margin: 0;
25
- min-height: 100vh;
26
- color: var(--text);
27
- font-family: Georgia, "Times New Roman", serif;
28
- background:
29
- radial-gradient(circle at top left, rgba(15, 118, 110, 0.18), transparent 28%),
30
- radial-gradient(circle at 80% 20%, rgba(154, 52, 18, 0.14), transparent 22%),
31
- linear-gradient(180deg, var(--bg-deep) 0%, var(--bg) 100%);
32
- }
33
-
34
- a {
35
- color: var(--accent);
36
- }
37
-
38
- .button-link {
39
- display: inline-flex;
40
- align-items: center;
41
- padding: 0.7rem 1rem;
42
- border-radius: 999px;
43
- background: var(--accent);
44
- color: #f8fbfb;
45
- text-decoration: none;
46
- }
47
-
48
- .shell {
49
- max-width: 920px;
50
- margin: 0 auto;
51
- padding: 56px 20px 80px;
52
- }
53
-
54
- .eyebrow {
55
- margin: 0 0 12px;
56
- letter-spacing: 0.08em;
57
- text-transform: uppercase;
58
- color: var(--accent-strong);
59
- font-size: 0.8rem;
60
- }
61
-
62
- .hero {
63
- margin-bottom: 28px;
64
- }
65
-
66
- .hero h1 {
67
- margin: 0;
68
- font-size: clamp(3rem, 9vw, 5.5rem);
69
- line-height: 0.95;
70
- }
71
-
72
- .hero p:last-child {
73
- max-width: 58ch;
74
- color: var(--muted);
75
- line-height: 1.8;
76
- font-size: 1.04rem;
77
- }
78
-
79
- .panel,
80
- .article-card {
81
- background: var(--panel);
82
- border: 1px solid var(--border);
83
- border-radius: 24px;
84
- box-shadow: var(--shadow);
85
- backdrop-filter: blur(10px);
86
- }
87
-
88
- .panel {
89
- padding: 24px;
90
- }
91
-
92
- .article-card {
93
- padding: 32px;
94
- background: var(--panel-strong);
95
- }
96
-
97
- .post-list {
98
- list-style: none;
99
- padding: 0;
100
- margin: 0;
101
- }
102
-
103
- .post-list li {
104
- display: flex;
105
- justify-content: space-between;
106
- align-items: baseline;
107
- gap: 20px;
108
- padding: 18px 0;
109
- border-bottom: 1px solid var(--border);
110
- }
111
-
112
- .post-list li:last-child {
113
- border-bottom: 0;
114
- }
115
-
116
- .post-list a {
117
- text-decoration: none;
118
- font-size: 1.15rem;
119
- }
120
-
121
- .post-list p {
122
- margin: 0.45rem 0 0;
123
- color: var(--muted);
124
- line-height: 1.7;
125
- max-width: 52ch;
126
- }
127
-
128
- .post-list span,
129
- .meta {
130
- color: var(--muted);
131
- }
132
-
133
- .tag-list {
134
- display: flex;
135
- flex-wrap: wrap;
136
- gap: 0.5rem;
137
- list-style: none;
138
- padding: 0;
139
- margin: 0 0 1.25rem;
140
- }
141
-
142
- .tag {
143
- padding: 0.35rem 0.7rem;
144
- border-radius: 999px;
145
- background: rgba(15, 118, 110, 0.1);
146
- color: var(--accent);
147
- font-size: 0.85rem;
148
- }
149
-
150
- .article-content p,
151
- .article-content li {
152
- line-height: 1.8;
2
+ background-color: #f6f4ee;
3
+ background-image:
4
+ linear-gradient(rgba(16, 16, 16, 0.08) 1px, transparent 1px),
5
+ linear-gradient(90deg, rgba(16, 16, 16, 0.08) 1px, transparent 1px),
6
+ radial-gradient(circle at top center, rgba(16, 16, 16, 0.045), transparent 36%);
7
+ background-size: 126px 126px, 126px 126px, auto;
153
8
  }
154
9
 
155
10
  .article-content h2,
156
11
  .article-content h3 {
157
- margin-top: 2rem;
158
- }
159
-
160
- .article-content ul,
161
- .article-content ol {
162
- padding-left: 1.2rem;
12
+ margin-top: 2.2rem;
163
13
  }
164
14
 
165
15
  .article-content pre {
166
16
  overflow-x: auto;
167
- padding: 16px;
168
- background: #241d18;
169
- color: #f7efe7;
170
- border-radius: 14px;
171
- }
172
-
173
- .article-content code {
174
- font-family: "SFMono-Regular", Consolas, monospace;
17
+ border-radius: 1rem;
18
+ background: #121212;
19
+ color: #f6f6f3;
20
+ padding: 1rem 1.125rem;
175
21
  }
176
22
 
177
23
  .article-content blockquote {
178
24
  margin: 1.5rem 0;
179
- padding: 0.1rem 0 0.1rem 1rem;
180
- border-left: 4px solid rgba(15, 118, 110, 0.35);
181
- color: var(--muted);
25
+ padding-left: 1rem;
26
+ border-left: 3px solid rgba(16, 16, 16, 0.16);
182
27
  }
183
28
 
184
- .archive-list {
185
- display: grid;
186
- gap: 1rem;
187
- }
188
-
189
- .archive-item {
190
- padding: 1.1rem 0;
191
- border-bottom: 1px solid var(--border);
192
- }
193
-
194
- .archive-item:last-child {
195
- border-bottom: 0;
196
- }
197
-
198
- .archive-item h2 {
199
- margin: 0;
200
- }
201
-
202
- .archive-item p {
203
- margin: 0.5rem 0 0;
204
- }
205
-
206
- .archive-meta {
207
- color: var(--muted);
208
- }
209
-
210
- @media (max-width: 640px) {
211
- .shell {
212
- padding-top: 40px;
213
- }
214
-
215
- .article-card,
216
- .panel {
217
- padding: 22px;
218
- }
219
-
220
- .post-list li {
221
- flex-direction: column;
222
- align-items: flex-start;
223
- }
29
+ .tag {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ border: 1px solid rgba(16, 16, 16, 0.1);
33
+ border-radius: 999px;
34
+ background: rgba(16, 16, 16, 0.04);
35
+ padding: 0.42rem 0.9rem;
36
+ font-size: 0.86rem;
37
+ color: #66645d;
224
38
  }
@@ -0,0 +1,5 @@
1
+ @import "tailwindcss";
2
+
3
+ @source "../templates";
4
+ @source "../content";
5
+ @source "../public";
@@ -6,17 +6,13 @@
6
6
  <title>{{title}}</title>
7
7
  <meta name="description" content="{{description}}">
8
8
  <link rel="stylesheet" href="/assets/site.css">
9
+ <link rel="stylesheet" href="/assets/overrides.css">
9
10
  </head>
10
- <body class="archive-page">
11
- <main class="shell">
12
- <section class="hero">
13
- <p class="eyebrow">Archive</p>
14
- <h1>All posts</h1>
15
- <p>Everything currently discoverable in the content folder, sorted newest first.</p>
16
- </section>
17
- <section class="panel archive-list">
18
- {{content}}
19
- </section>
11
+ <body class="bg-[#f6f4ee] px-4 py-4 text-[#101010] antialiased sm:px-6 lg:px-8">
12
+ <main class="mx-auto w-full max-w-6xl rounded-[2rem] border border-black/10 bg-white/70 px-6 py-8 shadow-[0_24px_70px_rgba(17,17,17,0.06)] backdrop-blur-xl sm:px-10">
13
+ <p class="mb-3 text-[0.72rem] uppercase tracking-[0.22em] text-[#66645d]">Archive</p>
14
+ <h1 class="max-w-[12ch] text-balance text-[clamp(2.2rem,4vw,3.6rem)] font-semibold leading-[0.98] tracking-[-0.08em] text-[#101010]">Everything currently published.</h1>
15
+ <div class="mt-8 space-y-4">{{content}}</div>
20
16
  </main>
21
17
  </body>
22
18
  </html>
@@ -6,20 +6,17 @@
6
6
  <title>{{title}}</title>
7
7
  <meta name="description" content="{{description}}">
8
8
  <link rel="stylesheet" href="/assets/site.css">
9
+ <link rel="stylesheet" href="/assets/overrides.css">
9
10
  </head>
10
- <body class="index-page">
11
- <main class="shell">
12
- <section class="hero">
13
- <p class="eyebrow">Minimal static blog engine</p>
14
- <h1>{{title}}</h1>
15
- <p>{{description}}</p>
16
- <p><a class="button-link" href="/archive">Browse the archive</a></p>
17
- </section>
18
-
19
- <section class="panel">
20
- <ul class="post-list">
21
- {{content}}
22
- </ul>
11
+ <body class="bg-[#f6f4ee] text-[#101010] antialiased">
12
+ <main class="mx-auto flex min-h-screen w-full max-w-6xl items-center px-4 py-4 sm:px-6 lg:px-8">
13
+ <section class="w-full rounded-[2rem] border border-black/10 bg-white/70 px-6 py-10 text-center shadow-[0_24px_70px_rgba(17,17,17,0.06)] backdrop-blur-xl sm:px-10 lg:px-16">
14
+ <p class="mb-4 text-[0.72rem] uppercase tracking-[0.22em] text-[#66645d]">Tailwind first</p>
15
+ <h1 class="mx-auto max-w-[80%] text-balance text-[clamp(2.35rem,5vw,4.2rem)] font-semibold leading-[0.98] tracking-[-0.08em] text-[#101010]">{{title}}</h1>
16
+ <p class="mx-auto mt-5 max-w-[70%] text-balance text-[clamp(0.98rem,1.25vw,1.05rem)] leading-7 text-[#66645d]">{{description}}</p>
17
+ <div class="mt-8 rounded-full border border-black/10 px-4 py-2 text-sm text-[#66645d]">
18
+ Edit Tailwind utilities in the templates and use <code class="font-mono text-[#101010]">public/assets/overrides.css</code> for custom CSS.
19
+ </div>
23
20
  </section>
24
21
  </main>
25
22
  </body>
@@ -6,18 +6,17 @@
6
6
  <title>{{title}}</title>
7
7
  <meta name="description" content="{{description}}">
8
8
  <link rel="stylesheet" href="/assets/site.css">
9
+ <link rel="stylesheet" href="/assets/overrides.css">
9
10
  </head>
10
- <body class="post-page">
11
- <main class="shell">
12
- <article class="article-card">
13
- <p class="eyebrow"><a href="/">Back to home</a></p>
14
- <h1>{{title}}</h1>
15
- <p class="meta">{{date}} · {{author}}</p>
16
- <ul class="tag-list">{{tags}}</ul>
17
- <div class="article-content">
18
- {{content}}
19
- </div>
20
- </article>
11
+ <body class="bg-[#f6f4ee] px-4 py-4 text-[#101010] antialiased sm:px-6 lg:px-8">
12
+ <main class="mx-auto w-full max-w-5xl rounded-[2rem] border border-black/10 bg-white/85 px-6 py-8 shadow-[0_24px_70px_rgba(17,17,17,0.06)] backdrop-blur-xl sm:px-10">
13
+ <p class="mb-4 text-[0.72rem] uppercase tracking-[0.22em] text-[#66645d]"><a class="transition hover:text-[#101010]" href="/">Back to home</a></p>
14
+ <h1 class="max-w-[12ch] text-balance text-[clamp(2.2rem,4vw,3.8rem)] font-semibold leading-[0.98] tracking-[-0.08em] text-[#101010]">{{title}}</h1>
15
+ <p class="mt-4 text-sm text-[#66645d]">{{date}} · {{author}}</p>
16
+ <ul class="mb-7 mt-4 flex flex-wrap gap-2">{{tags}}</ul>
17
+ <div class="article-content max-w-none">
18
+ {{content}}
19
+ </div>
21
20
  </main>
22
21
  </body>
23
22
  </html>