@bivabdas/sharq 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bin/create-sharq.js +33 -0
- package/bin/sharq.js +57 -0
- package/index.js +4 -0
- package/lib/config.js +51 -0
- package/lib/generator.js +172 -0
- package/lib/package-info.js +8 -0
- package/lib/renderer.js +298 -0
- package/lib/scaffold.js +161 -0
- package/lib/server.js +199 -0
- package/lib/sitemap.js +21 -0
- package/package.json +36 -0
- package/scaffold/template/content/hello-sharq.md +17 -0
- package/scaffold/template/public/assets/site.css +224 -0
- package/scaffold/template/templates/archive.html +22 -0
- package/scaffold/template/templates/index.html +26 -0
- package/scaffold/template/templates/post.html +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# sharq
|
|
2
|
+
|
|
3
|
+
`sharq` is a minimal static rendering framework for landing pages and Markdown-driven blogs. It generates crawl-friendly HTML, keeps output on disk, and can scaffold a new site with a single command.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Markdown posts with frontmatter
|
|
8
|
+
- Static HTML output in `public/`
|
|
9
|
+
- On-demand post generation
|
|
10
|
+
- Index, archive, and sitemap generation
|
|
11
|
+
- Zero-dependency runtime on native Node.js APIs
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install sharq
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
sharq start
|
|
23
|
+
sharq dev
|
|
24
|
+
sharq build
|
|
25
|
+
sharq create my-site
|
|
26
|
+
create-sharq my-site
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`sharq build` creates a fresh `sharq/` folder with deployable static files:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
sharq/
|
|
33
|
+
index.html
|
|
34
|
+
archive.html
|
|
35
|
+
sitemap.xml
|
|
36
|
+
assets/
|
|
37
|
+
blog/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Content format
|
|
41
|
+
|
|
42
|
+
Posts live in `content/*.md` and use frontmatter:
|
|
43
|
+
|
|
44
|
+
```md
|
|
45
|
+
---
|
|
46
|
+
title: Hello sharq
|
|
47
|
+
description: Your first sharq post.
|
|
48
|
+
date: 2026-04-04
|
|
49
|
+
author: You
|
|
50
|
+
tags: welcome, getting-started
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# Hello sharq
|
|
54
|
+
|
|
55
|
+
This site was scaffolded with sharq.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Project structure
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
content/
|
|
62
|
+
templates/
|
|
63
|
+
public/
|
|
64
|
+
sharq.config.js
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
export default {
|
|
71
|
+
siteTitle: "My Site",
|
|
72
|
+
siteDescription: "A sharq site",
|
|
73
|
+
siteUrl: "https://example.com"
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Publish checklist
|
|
78
|
+
|
|
79
|
+
- Set `siteUrl` in `sharq.config.js`
|
|
80
|
+
- Add your content in `content/`
|
|
81
|
+
- Customize templates in `templates/`
|
|
82
|
+
- Run `npm run build`
|
|
83
|
+
- Ship the generated `sharq/` output with your hosting flow
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { readPackageVersion } from "../lib/package-info.js";
|
|
6
|
+
import { scaffoldProject } from "../lib/scaffold.js";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
11
|
+
const projectName = process.argv[2];
|
|
12
|
+
const packageVersion = await readPackageVersion(packageRoot);
|
|
13
|
+
|
|
14
|
+
if (projectName === "--help" || projectName === "-h" || projectName === "help") {
|
|
15
|
+
console.log(`create-sharq v${packageVersion}
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
create-sharq [project-name]
|
|
19
|
+
`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const project = await scaffoldProject({
|
|
24
|
+
packageRoot,
|
|
25
|
+
frameworkVersion: `^${packageVersion}`,
|
|
26
|
+
projectName
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
console.log(`\nCreated ${project.projectName} at ${project.projectDir}`);
|
|
30
|
+
console.log("\nNext steps:");
|
|
31
|
+
console.log(` cd ${project.projectName}`);
|
|
32
|
+
console.log(" npm install");
|
|
33
|
+
console.log(" npm run dev");
|
package/bin/sharq.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { resolveConfig } from "../lib/config.js";
|
|
6
|
+
import { buildStaticSite } from "../lib/generator.js";
|
|
7
|
+
import { readPackageVersion } from "../lib/package-info.js";
|
|
8
|
+
import { startServer } from "../lib/server.js";
|
|
9
|
+
import { scaffoldProject } from "../lib/scaffold.js";
|
|
10
|
+
|
|
11
|
+
const [, , command, projectName] = process.argv;
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
15
|
+
const packageVersion = await readPackageVersion(packageRoot);
|
|
16
|
+
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(`sharq v${packageVersion}
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
sharq
|
|
22
|
+
sharq start
|
|
23
|
+
sharq dev
|
|
24
|
+
sharq build
|
|
25
|
+
sharq create [project-name]
|
|
26
|
+
|
|
27
|
+
Commands:
|
|
28
|
+
start Start the local sharq server
|
|
29
|
+
dev Alias for start, intended for local development
|
|
30
|
+
build Generate a deployable static site in ./sharq
|
|
31
|
+
create Scaffold a new sharq site
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!command || command === "dev" || command === "start") {
|
|
36
|
+
await startServer();
|
|
37
|
+
} else if (command === "build") {
|
|
38
|
+
const config = await resolveConfig();
|
|
39
|
+
await buildStaticSite(config);
|
|
40
|
+
} else if (command === "--help" || command === "-h" || command === "help") {
|
|
41
|
+
printHelp();
|
|
42
|
+
} else if (command === "create") {
|
|
43
|
+
const project = await scaffoldProject({
|
|
44
|
+
packageRoot,
|
|
45
|
+
frameworkVersion: `^${packageVersion}`,
|
|
46
|
+
projectName
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
console.log(`\nCreated ${project.projectName} at ${project.projectDir}`);
|
|
50
|
+
console.log("\nNext steps:");
|
|
51
|
+
console.log(` cd ${project.projectName}`);
|
|
52
|
+
console.log(" npm install");
|
|
53
|
+
console.log(" npm run dev");
|
|
54
|
+
} else {
|
|
55
|
+
console.error(`Unknown sharq command: ${command}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { resolveConfig, createConfig } from "./lib/config.js";
|
|
2
|
+
export { buildStaticSite, generatePost, refreshSiteArtifacts, getAllPosts, ensurePostGenerated } from "./lib/generator.js";
|
|
3
|
+
export { startServer } from "./lib/server.js";
|
|
4
|
+
export { scaffoldProject } from "./lib/scaffold.js";
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
export function createConfig(rootDir, overrides = {}) {
|
|
6
|
+
const buildDirName = overrides.buildDirName || "sharq";
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
rootDir,
|
|
10
|
+
siteUrl: overrides.siteUrl || process.env.SITE_URL || "http://localhost:3000",
|
|
11
|
+
siteTitle: overrides.siteTitle || "sharq",
|
|
12
|
+
siteDescription:
|
|
13
|
+
overrides.siteDescription || "A tiny publishing surface that renders posts from Markdown and serves static HTML.",
|
|
14
|
+
buildDirName,
|
|
15
|
+
contentDir: path.join(rootDir, "content"),
|
|
16
|
+
templatesDir: path.join(rootDir, "templates"),
|
|
17
|
+
publicDir: path.join(rootDir, "public"),
|
|
18
|
+
blogOutputDir: path.join(rootDir, "public", "blog"),
|
|
19
|
+
sitemapPath: path.join(rootDir, "public", "sitemap.xml"),
|
|
20
|
+
homePagePath: path.join(rootDir, "public", "index.html"),
|
|
21
|
+
archivePagePath: path.join(rootDir, "public", "archive.html"),
|
|
22
|
+
staticAssetsDir: path.join(rootDir, "public", "assets"),
|
|
23
|
+
buildDir: path.join(rootDir, buildDirName)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadUserConfig(rootDir) {
|
|
28
|
+
const configPaths = [path.join(rootDir, "sharq.config.js"), path.join(rootDir, "statiq.config.js")];
|
|
29
|
+
|
|
30
|
+
for (const configPath of configPaths) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(configPath);
|
|
33
|
+
const module = await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`);
|
|
34
|
+
const loaded = module.default ?? module.config ?? {};
|
|
35
|
+
return typeof loaded === "function" ? loaded() : loaded;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code === "ENOENT") {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function resolveConfig(rootDir = process.cwd()) {
|
|
49
|
+
const userConfig = await loadUserConfig(rootDir);
|
|
50
|
+
return createConfig(rootDir, userConfig);
|
|
51
|
+
}
|
package/lib/generator.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { generateSitemap } from "./sitemap.js";
|
|
4
|
+
import { readPostSource, renderArchivePage, renderIndexPage, renderPostPage } from "./renderer.js";
|
|
5
|
+
|
|
6
|
+
async function ensureDirectories(config) {
|
|
7
|
+
await Promise.all([
|
|
8
|
+
fs.mkdir(config.contentDir, { recursive: true }),
|
|
9
|
+
fs.mkdir(config.publicDir, { recursive: true }),
|
|
10
|
+
fs.mkdir(config.blogOutputDir, { recursive: true }),
|
|
11
|
+
fs.mkdir(config.templatesDir, { recursive: true }),
|
|
12
|
+
fs.mkdir(config.staticAssetsDir, { recursive: true })
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function assertValidSlug(slug) {
|
|
17
|
+
if (!/^[a-z0-9-]+$/i.test(slug)) {
|
|
18
|
+
throw new Error("Invalid slug format.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function listContentSlugs(config) {
|
|
23
|
+
await ensureDirectories(config);
|
|
24
|
+
const entries = await fs.readdir(config.contentDir, { withFileTypes: true });
|
|
25
|
+
|
|
26
|
+
return entries
|
|
27
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
28
|
+
.map((entry) => entry.name.replace(/\.md$/, ""));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getAllPosts(config) {
|
|
32
|
+
const slugs = await listContentSlugs(config);
|
|
33
|
+
const posts = await Promise.all(slugs.map((slug) => readPostSource(config, slug)));
|
|
34
|
+
|
|
35
|
+
return posts.sort((left, right) => right.date.localeCompare(left.date));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function generatePost(config, slug) {
|
|
39
|
+
await ensureDirectories(config);
|
|
40
|
+
assertValidSlug(slug);
|
|
41
|
+
|
|
42
|
+
const post = await readPostSource(config, slug);
|
|
43
|
+
const html = await renderPostPage(config, post);
|
|
44
|
+
const outputPath = path.join(config.blogOutputDir, `${slug}.html`);
|
|
45
|
+
|
|
46
|
+
await fs.writeFile(outputPath, html, "utf8");
|
|
47
|
+
console.log(`[sharq] generated /blog/${slug}`);
|
|
48
|
+
await refreshSiteArtifacts(config);
|
|
49
|
+
|
|
50
|
+
return { ...post, outputPath, html };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function refreshSiteArtifacts(config) {
|
|
54
|
+
const posts = await getAllPosts(config);
|
|
55
|
+
const indexHtml = await renderIndexPage(config, posts);
|
|
56
|
+
const archiveHtml = await renderArchivePage(config, posts);
|
|
57
|
+
|
|
58
|
+
await fs.writeFile(config.homePagePath, indexHtml, "utf8");
|
|
59
|
+
await fs.writeFile(config.archivePagePath, archiveHtml, "utf8");
|
|
60
|
+
await generateSitemap(config, posts);
|
|
61
|
+
console.log(`[sharq] refreshed homepage and sitemap for ${posts.length} post${posts.length === 1 ? "" : "s"}`);
|
|
62
|
+
|
|
63
|
+
return posts;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function copyDirectory(sourceDir, targetDir) {
|
|
67
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
68
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
72
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
73
|
+
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
await copyDirectory(sourcePath, targetPath);
|
|
76
|
+
} else {
|
|
77
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function copyStaticFiles(config, outputDir) {
|
|
83
|
+
try {
|
|
84
|
+
const entries = await fs.readdir(config.publicDir, { withFileTypes: true });
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.name === "blog") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (["index.html", "archive.html", "sitemap.xml"].includes(entry.name)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sourcePath = path.join(config.publicDir, entry.name);
|
|
96
|
+
const targetPath = path.join(outputDir, entry.name);
|
|
97
|
+
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
await copyDirectory(sourcePath, targetPath);
|
|
100
|
+
} else {
|
|
101
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code !== "ENOENT") {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createOutputConfig(config, outputDir) {
|
|
112
|
+
return {
|
|
113
|
+
...config,
|
|
114
|
+
publicDir: outputDir,
|
|
115
|
+
blogOutputDir: path.join(outputDir, "blog"),
|
|
116
|
+
sitemapPath: path.join(outputDir, "sitemap.xml"),
|
|
117
|
+
homePagePath: path.join(outputDir, "index.html"),
|
|
118
|
+
archivePagePath: path.join(outputDir, "archive.html"),
|
|
119
|
+
staticAssetsDir: path.join(outputDir, "assets")
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function buildStaticSite(config, outputDir = config.buildDir) {
|
|
124
|
+
const outputConfig = createOutputConfig(config, outputDir);
|
|
125
|
+
|
|
126
|
+
await fs.rm(outputDir, { recursive: true, force: true });
|
|
127
|
+
await Promise.all([
|
|
128
|
+
fs.mkdir(outputConfig.publicDir, { recursive: true }),
|
|
129
|
+
fs.mkdir(outputConfig.blogOutputDir, { recursive: true })
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
await copyStaticFiles(config, outputDir);
|
|
133
|
+
|
|
134
|
+
const posts = await getAllPosts(config);
|
|
135
|
+
await Promise.all(
|
|
136
|
+
posts.map(async (post) => {
|
|
137
|
+
const postHtml = await renderPostPage(outputConfig, post);
|
|
138
|
+
const outputPath = path.join(outputConfig.blogOutputDir, `${post.slug}.html`);
|
|
139
|
+
await fs.writeFile(outputPath, postHtml, "utf8");
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const indexHtml = await renderIndexPage(outputConfig, posts);
|
|
144
|
+
const archiveHtml = await renderArchivePage(outputConfig, posts);
|
|
145
|
+
|
|
146
|
+
await fs.writeFile(outputConfig.homePagePath, indexHtml, "utf8");
|
|
147
|
+
await fs.writeFile(outputConfig.archivePagePath, archiveHtml, "utf8");
|
|
148
|
+
await generateSitemap(outputConfig, posts);
|
|
149
|
+
|
|
150
|
+
console.log(`[sharq] built ${posts.length} post${posts.length === 1 ? "" : "s"} to ${outputDir}`);
|
|
151
|
+
return { outputDir, posts };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function ensurePostGenerated(config, slug) {
|
|
155
|
+
assertValidSlug(slug);
|
|
156
|
+
const outputPath = path.join(config.blogOutputDir, `${slug}.html`);
|
|
157
|
+
const sourcePath = path.join(config.contentDir, `${slug}.md`);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const [sourceStats, outputStats] = await Promise.all([fs.stat(sourcePath), fs.stat(outputPath)]);
|
|
161
|
+
|
|
162
|
+
if (sourceStats.mtimeMs > outputStats.mtimeMs) {
|
|
163
|
+
const post = await generatePost(config, slug);
|
|
164
|
+
return post.outputPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return outputPath;
|
|
168
|
+
} catch {
|
|
169
|
+
const post = await generatePost(config, slug);
|
|
170
|
+
return post.outputPath;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function readPackageVersion(packageRoot) {
|
|
5
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
6
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
7
|
+
return packageJson.version;
|
|
8
|
+
}
|