Dafa commited on
Commit
5878f15
·
verified ·
1 Parent(s): c1f29e6

Upload 7 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build untuk HF Spaces
2
+ # Stage 1: Build Next.js
3
+ FROM node:18-alpine AS nextjs-builder
4
+
5
+ WORKDIR /app
6
+
7
+ # Install dependencies
8
+ COPY package.json package-lock.json* ./
9
+ RUN npm ci --only=production
10
+
11
+ # Copy source dan build
12
+ COPY . .
13
+ RUN npm run build
14
+
15
+ # Stage 2: Jekyll + Ruby
16
+ FROM ruby:3.1-alpine AS final
17
+
18
+ # Install system dependencies
19
+ RUN apk add --no-cache \
20
+ build-base \
21
+ git \
22
+ nodejs \
23
+ npm \
24
+ curl \
25
+ bash \
26
+ tzdata
27
+
28
+ # Install Jekyll dan Bundler
29
+ RUN gem install jekyll bundler && gem cleanup
30
+
31
+ # Set working directory
32
+ WORKDIR /app
33
+
34
+ # Copy Next.js build dari stage sebelumnya
35
+ COPY --from=nextjs-builder /app/.next ./.next
36
+ COPY --from=nextjs-builder /app/node_modules ./node_modules
37
+ COPY --from=nextjs-builder /app/package.json ./package.json
38
+ COPY --from=nextjs-builder /app/public ./public
39
+
40
+ # Copy sisa files
41
+ COPY . .
42
+
43
+ # Create user dan directories untuk HF Spaces
44
+ RUN addgroup -g 1000 appuser && \
45
+ adduser -D -s /bin/bash -u 1000 -G appuser appuser
46
+
47
+ # Create directories dengan proper permissions
48
+ RUN mkdir -p /app/projects /app/templates /app/.next && \
49
+ chmod -R 755 /app && \
50
+ chown -R appuser:appuser /app
51
+
52
+ # Copy dan setup entrypoint
53
+ COPY entrypoint-hf.sh /usr/local/bin/entrypoint.sh
54
+ RUN chmod +x /usr/local/bin/entrypoint.sh && \
55
+ chown appuser:appuser /usr/local/bin/entrypoint.sh
56
+
57
+ # Switch ke non-root user
58
+ USER appuser
59
+
60
+ # Expose port untuk HF Spaces
61
+ EXPOSE 7860
62
+
63
+ # Environment variables
64
+ ENV PORT=7860
65
+ ENV NODE_ENV=production
66
+ ENV JEKYLL_ENV=production
67
+ ENV NEXT_TELEMETRY_DISABLED=1
68
+ ENV CI=true
69
+
70
+ # Health check
71
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
72
+ CMD curl -f http://localhost:7860/api/health || exit 1
73
+
74
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
75
+ CMD ["start"]
next.config.js ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ swcMinify: true,
5
+ output: 'standalone',
6
+
7
+ // Disable problematic features untuk HF Spaces
8
+ telemetry: {
9
+ enabled: false
10
+ },
11
+
12
+ // Environment variables
13
+ env: {
14
+ JEKYLL_ENV: process.env.JEKYLL_ENV || 'production',
15
+ GEMINI_API_KEY: process.env.GEMINI_API_KEY,
16
+ NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
17
+ NEXT_TELEMETRY_DISABLED: '1',
18
+ },
19
+
20
+ // API routes configuration
21
+ async rewrites() {
22
+ return [
23
+ // Jekyll preview proxy
24
+ {
25
+ source: '/preview/:site/:path*',
26
+ destination: '/api/jekyll/preview/:site/:path*'
27
+ },
28
+ // Static jekyll files
29
+ {
30
+ source: '/sites/:site/:path*',
31
+ destination: '/api/jekyll/static/:site/:path*'
32
+ }
33
+ ];
34
+ },
35
+
36
+ // Headers untuk CORS dan security
37
+ async headers() {
38
+ return [
39
+ {
40
+ source: '/api/:path*',
41
+ headers: [
42
+ {
43
+ key: 'Access-Control-Allow-Origin',
44
+ value: process.env.NODE_ENV === 'production'
45
+ ? process.env.ALLOWED_ORIGINS || '*'
46
+ : '*'
47
+ },
48
+ {
49
+ key: 'Access-Control-Allow-Methods',
50
+ value: 'GET, POST, PUT, DELETE, OPTIONS'
51
+ },
52
+ {
53
+ key: 'Access-Control-Allow-Headers',
54
+ value: 'Content-Type, Authorization, X-Requested-With'
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ source: '/sites/:path*',
60
+ headers: [
61
+ {
62
+ key: 'Cache-Control',
63
+ value: 'public, max-age=3600, s-maxage=3600'
64
+ }
65
+ ]
66
+ }
67
+ ];
68
+ },
69
+
70
+ // Webpack config untuk Jekyll integration
71
+ webpack: (config, { isServer }) => {
72
+ if (!isServer) {
73
+ config.resolve.fallback = {
74
+ ...config.resolve.fallback,
75
+ fs: false,
76
+ path: false,
77
+ child_process: false,
78
+ };
79
+ }
80
+
81
+ // Ignore Jekyll files in webpack
82
+ config.watchOptions = {
83
+ ...config.watchOptions,
84
+ ignored: [
85
+ '**/projects/**',
86
+ '**/templates/**',
87
+ '**/.bundle/**'
88
+ ]
89
+ };
90
+
91
+ return config;
92
+ },
93
+
94
+ // Image optimization
95
+ images: {
96
+ unoptimized: true,
97
+ domains: ['localhost', 'huggingface.co'],
98
+ },
99
+
100
+ // Build optimization
101
+ compress: true,
102
+ poweredByHeader: false,
103
+
104
+ typescript: {
105
+ ignoreBuildErrors: true,
106
+ },
107
+ eslint: {
108
+ ignoreDuringBuilds: true,
109
+ },
110
+
111
+ // Experimental features
112
+ experimental: {
113
+ serverComponentsExternalPackages: ['@google/generative-ai']
114
+ }
115
+ };
116
+
117
+ module.exports = nextConfig;
package.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "jekyll-studio-hf",
3
+ "version": "1.0.0",
4
+ "description": "Jekyll Studio untuk Hugging Face Spaces",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint",
11
+ "jekyll:create": "/usr/local/bin/entrypoint.sh create-site",
12
+ "jekyll:build": "/usr/local/bin/entrypoint.sh build-site",
13
+ "jekyll:serve": "/usr/local/bin/entrypoint.sh serve-site"
14
+ },
15
+ "dependencies": {
16
+ "next": "^14.0.0",
17
+ "react": "^18.0.0",
18
+ "react-dom": "^18.0.0",
19
+ "@google/generative-ai": "^0.2.0",
20
+ "next-auth": "^4.24.0",
21
+ "uuid": "^9.0.0",
22
+ "formidable": "^3.5.0",
23
+ "fs-extra": "^11.1.0",
24
+ "archiver": "^6.0.0",
25
+ "yauzl": "^2.10.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.0.0",
29
+ "@types/react": "^18.0.0",
30
+ "@types/react-dom": "^18.0.0",
31
+ "eslint": "^8.0.0",
32
+ "eslint-config-next": "^14.0.0",
33
+ "typescript": "^5.0.0"
34
+ },
35
+ "keywords": [
36
+ "jekyll",
37
+ "nextjs",
38
+ "huggingface",
39
+ "static-site-generator"
40
+ ],
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ }
44
+ }
pages/api/jekyll/build.js ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pages/api/jekyll/build.js
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import path from 'path';
5
+ import fs from 'fs-extra';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ export default async function handler(req, res) {
10
+ if (req.method !== 'POST') {
11
+ return res.status(405).json({ error: 'Method not allowed' });
12
+ }
13
+
14
+ try {
15
+ const { siteName } = req.body;
16
+
17
+ if (!siteName) {
18
+ return res.status(400).json({ error: 'Site name is required' });
19
+ }
20
+
21
+ const sitePath = path.join('/app/projects', siteName);
22
+
23
+ // Check if site exists
24
+ if (!(await fs.pathExists(sitePath))) {
25
+ return res.status(404).json({ error: 'Site not found' });
26
+ }
27
+
28
+ console.log(`Building Jekyll site: ${siteName}`);
29
+
30
+ // Build Jekyll site menggunakan entrypoint script
31
+ const { stdout, stderr } = await execAsync(
32
+ `/usr/local/bin/entrypoint.sh build-site ${sitePath}`,
33
+ {
34
+ timeout: 60000, // 1 minute timeout untuk build
35
+ cwd: '/app'
36
+ }
37
+ );
38
+
39
+ // Check if build was successful
40
+ const siteOutputPath = path.join(sitePath, '_site');
41
+ const buildExists = await fs.pathExists(siteOutputPath);
42
+
43
+ if (!buildExists) {
44
+ throw new Error('Build completed but _site directory not found');
45
+ }
46
+
47
+ // Get build info
48
+ const stats = await fs.stat(siteOutputPath);
49
+ const files = await getAllFiles(siteOutputPath);
50
+
51
+ console.log('Jekyll site built:', stdout);
52
+ if (stderr) console.warn('Jekyll build warnings:', stderr);
53
+
54
+ res.status(200).json({
55
+ success: true,
56
+ message: 'Jekyll site built successfully',
57
+ siteName,
58
+ buildPath: siteOutputPath,
59
+ buildTime: stats.mtime,
60
+ filesCount: files.length,
61
+ previewUrl: `/sites/${siteName}`,
62
+ output: stdout,
63
+ warnings: stderr || null
64
+ });
65
+
66
+ } catch (error) {
67
+ console.error('Error building Jekyll site:', error);
68
+
69
+ res.status(500).json({
70
+ error: 'Failed to build Jekyll site',
71
+ details: error.message,
72
+ stderr: error.stderr || null
73
+ });
74
+ }
75
+ }
76
+
77
+ // Helper function untuk get semua files di directory
78
+ async function getAllFiles(dir) {
79
+ const files = [];
80
+
81
+ async function scan(currentDir) {
82
+ const items = await fs.readdir(currentDir);
83
+
84
+ for (const item of items) {
85
+ const fullPath = path.join(currentDir, item);
86
+ const stat = await fs.stat(fullPath);
87
+
88
+ if (stat.isDirectory()) {
89
+ await scan(fullPath);
90
+ } else {
91
+ files.push(fullPath);
92
+ }
93
+ }
94
+ }
95
+
96
+ await scan(dir);
97
+ return files;
98
+ }
pages/api/jekyll/create.js ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pages/api/jekyll/create.js
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import path from 'path';
5
+ import fs from 'fs-extra';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ export default async function handler(req, res) {
10
+ if (req.method !== 'POST') {
11
+ return res.status(405).json({ error: 'Method not allowed' });
12
+ }
13
+
14
+ try {
15
+ const { siteName, template = 'blog', config = {} } = req.body;
16
+
17
+ if (!siteName || !/^[a-zA-Z0-9-_]+$/.test(siteName)) {
18
+ return res.status(400).json({
19
+ error: 'Invalid site name. Use only alphanumeric, dash, and underscore.'
20
+ });
21
+ }
22
+
23
+ const sitePath = path.join('/app/projects', siteName);
24
+
25
+ // Check if site already exists
26
+ if (await fs.pathExists(sitePath)) {
27
+ return res.status(409).json({
28
+ error: 'Site already exists'
29
+ });
30
+ }
31
+
32
+ // Create Jekyll site menggunakan entrypoint script
33
+ console.log(`Creating Jekyll site: ${siteName}`);
34
+
35
+ const { stdout, stderr } = await execAsync(
36
+ `/usr/local/bin/entrypoint.sh create-site ${siteName}`,
37
+ {
38
+ timeout: 30000,
39
+ cwd: '/app'
40
+ }
41
+ );
42
+
43
+ // Customize site based on template
44
+ await customizeJekyllSite(sitePath, template, config);
45
+
46
+ console.log('Jekyll site created:', stdout);
47
+ if (stderr) console.warn('Jekyll warnings:', stderr);
48
+
49
+ res.status(201).json({
50
+ success: true,
51
+ message: 'Jekyll site created successfully',
52
+ siteName,
53
+ path: sitePath,
54
+ previewUrl: `/preview/${siteName}`,
55
+ output: stdout
56
+ });
57
+
58
+ } catch (error) {
59
+ console.error('Error creating Jekyll site:', error);
60
+
61
+ res.status(500).json({
62
+ error: 'Failed to create Jekyll site',
63
+ details: error.message,
64
+ stderr: error.stderr || null
65
+ });
66
+ }
67
+ }
68
+
69
+ async function customizeJekyllSite(sitePath, template, config) {
70
+ try {
71
+ // Update _config.yml dengan konfigurasi custom
72
+ const configPath = path.join(sitePath, '_config.yml');
73
+ const defaultConfig = await fs.readFile(configPath, 'utf8');
74
+
75
+ const customConfig = `${defaultConfig}
76
+
77
+ # Jekyll Studio Configuration
78
+ title: ${config.title || 'My Jekyll Site'}
79
+ description: ${config.description || 'Created with Jekyll Studio'}
80
+ url: ""
81
+ baseurl: ""
82
+
83
+ # Build settings
84
+ markdown: kramdown
85
+ highlighter: rouge
86
+ theme: minima
87
+
88
+ plugins:
89
+ - jekyll-feed
90
+
91
+ # Exclude from processing
92
+ exclude:
93
+ - Gemfile
94
+ - Gemfile.lock
95
+ - node_modules
96
+ - vendor/bundle/
97
+ - vendor/cache/
98
+ - vendor/gems/
99
+ - vendor/ruby/
100
+ `;
101
+
102
+ await fs.writeFile(configPath, customConfig);
103
+
104
+ // Create sample post berdasarkan template
105
+ const postsDir = path.join(sitePath, '_posts');
106
+ await fs.ensureDir(postsDir);
107
+
108
+ const samplePost = `---
109
+ layout: post
110
+ title: "Welcome to Jekyll Studio!"
111
+ date: ${new Date().toISOString().split('T')[0]} 12:00:00 +0000
112
+ categories: jekyll update
113
+ ---
114
+
115
+ # Welcome to Your New Jekyll Site!
116
+
117
+ This site was created using **Jekyll Studio** on Hugging Face Spaces.
118
+
119
+ ## Features
120
+
121
+ - 🚀 Fast static site generation
122
+ - 📝 Markdown support
123
+ - 🎨 Customizable themes
124
+ - 📱 Mobile responsive
125
+ - 🔍 SEO optimized
126
+
127
+ ## Getting Started
128
+
129
+ Edit this post in \`_posts/\` directory or create new posts using the Jekyll Studio interface.
130
+
131
+ Happy blogging! ✨
132
+ `;
133
+
134
+ const postPath = path.join(postsDir, `${new Date().toISOString().split('T')[0]}-welcome-to-jekyll-studio.md`);
135
+ await fs.writeFile(postPath, samplePost);
136
+
137
+ console.log(`Jekyll site customized with ${template} template`);
138
+ } catch (error) {
139
+ console.warn('Failed to customize Jekyll site:', error.message);
140
+ }
141
+ }
pages/api/jekyll/static/[...path].js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pages/api/jekyll/static/[...path].js
2
+ // Serve static Jekyll files
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import { lookup } from 'mime-types';
6
+
7
+ export default async function handler(req, res) {
8
+ try {
9
+ const { path: pathArray } = req.query;
10
+
11
+ if (!pathArray || !Array.isArray(pathArray) || pathArray.length === 0) {
12
+ return res.status(400).json({ error: 'Invalid path' });
13
+ }
14
+
15
+ const [siteName, ...filePath] = pathArray;
16
+ const requestedFile = filePath.join('/') || 'index.html';
17
+
18
+ // Security: prevent path traversal
19
+ if (siteName.includes('..') || requestedFile.includes('..')) {
20
+ return res.status(403).json({ error: 'Access denied' });
21
+ }
22
+
23
+ const sitePath = path.join('/app/projects', siteName, '_site');
24
+ const fullPath = path.join(sitePath, requestedFile);
25
+
26
+ // Check if site exists
27
+ if (!(await fs.pathExists(sitePath))) {
28
+ return res.status(404).json({ error: 'Site not found' });
29
+ }
30
+
31
+ // Try to find the file
32
+ let targetFile = fullPath;
33
+
34
+ // If requesting a directory, try index.html
35
+ if ((await fs.pathExists(fullPath)) && (await fs.stat(fullPath)).isDirectory()) {
36
+ targetFile = path.join(fullPath, 'index.html');
37
+ }
38
+
39
+ // If file doesn't exist, try adding .html extension
40
+ if (!(await fs.pathExists(targetFile))) {
41
+ targetFile = fullPath + '.html';
42
+ }
43
+
44
+ // Final check
45
+ if (!(await fs.pathExists(targetFile))) {
46
+ return res.status(404).json({ error: 'File not found' });
47
+ }
48
+
49
+ // Security: ensure file is within site directory
50
+ const resolvedPath = path.resolve(targetFile);
51
+ const resolvedSitePath = path.resolve(sitePath);
52
+
53
+ if (!resolvedPath.startsWith(resolvedSitePath)) {
54
+ return res.status(403).json({ error: 'Access denied' });
55
+ }
56
+
57
+ // Get file info
58
+ const stats = await fs.stat(targetFile);
59
+ const mimeType = lookup(targetFile) || 'application/octet-stream';
60
+
61
+ // Set headers
62
+ res.setHeader('Content-Type', mimeType);
63
+ res.setHeader('Content-Length', stats.size);
64
+ res.setHeader('Last-Modified', stats.mtime.toUTCString());
65
+ res.setHeader('Cache-Control', 'public, max-age=3600');
66
+
67
+ // Handle conditional requests
68
+ const ifModifiedSince = req.headers['if-modified-since'];
69
+ if (ifModifiedSince && new Date(ifModifiedSince) >= stats.mtime) {
70
+ return res.status(304).end();
71
+ }
72
+
73
+ // Stream the file
74
+ const fileStream = fs.createReadStream(targetFile);
75
+ fileStream.pipe(res);
76
+
77
+ fileStream.on('error', (error) => {
78
+ console.error('Error streaming file:', error);
79
+ if (!res.headersSent) {
80
+ res.status(500).json({ error: 'Error reading file' });
81
+ }
82
+ });
83
+
84
+ } catch (error) {
85
+ console.error('Error serving static file:', error);
86
+
87
+ if (!res.headersSent) {
88
+ res.status(500).json({
89
+ error: 'Internal server error',
90
+ details: error.message
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ export const config = {
97
+ api: {
98
+ responseLimit: '10mb', // Allow larger files
99
+ },
100
+ };
usr/local/bin/entrypoint.sh ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Function to create Jekyll site via API
5
+ create_jekyll_site() {
6
+ local site_name="$1"
7
+ local site_dir="/app/projects/$site_name"
8
+
9
+ echo "Creating Jekyll site: $site_name"
10
+
11
+ # Create directory
12
+ mkdir -p "$site_dir"
13
+ cd "$site_dir"
14
+
15
+ # Initialize Jekyll site
16
+ if [ ! -f "_config.yml" ]; then
17
+ jekyll new . --force --blank
18
+
19
+ # Create basic Gemfile for HF compatibility
20
+ cat > Gemfile << 'EOF'
21
+ source "https://rubygems.org"
22
+ gem "jekyll", "~> 4.3"
23
+ gem "webrick", "~> 1.7"
24
+ group :jekyll_plugins do
25
+ gem "jekyll-feed", "~> 0.12"
26
+ end
27
+ EOF
28
+
29
+ bundle install --path .bundle
30
+ echo "Jekyll site created successfully"
31
+ else
32
+ echo "Jekyll site already exists"
33
+ fi
34
+ }
35
+
36
+ # Function to build Jekyll site
37
+ build_jekyll_site() {
38
+ local site_dir="$1"
39
+
40
+ echo "Building Jekyll site in: $site_dir"
41
+ cd "$site_dir"
42
+
43
+ if [ -f "Gemfile" ]; then
44
+ bundle install --path .bundle
45
+ JEKYLL_ENV=production bundle exec jekyll build
46
+ else
47
+ jekyll build
48
+ fi
49
+
50
+ echo "Jekyll site built successfully"
51
+ }
52
+
53
+ # Function to serve Jekyll site (untuk development)
54
+ serve_jekyll_site() {
55
+ local site_dir="$1"
56
+ local port="${2:-4000}"
57
+
58
+ echo "Serving Jekyll site from: $site_dir on port: $port"
59
+ cd "$site_dir"
60
+
61
+ if [ -f "Gemfile" ]; then
62
+ bundle install --path .bundle
63
+ bundle exec jekyll serve --host 0.0.0.0 --port "$port"
64
+ else
65
+ jekyll serve --host 0.0.0.0 --port "$port"
66
+ fi
67
+ }
68
+
69
+ # Initialize environment
70
+ init_environment() {
71
+ echo "Initializing Jekyll Studio environment for Hugging Face Spaces..."
72
+
73
+ # Create necessary directories
74
+ mkdir -p /app/projects /app/templates
75
+
76
+ # Set proper permissions
77
+ chmod -R 755 /app/projects /app/templates 2>/dev/null || true
78
+
79
+ # Set default environment variables
80
+ export NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-"hf-spaces-jekyll-studio-secret"}
81
+ export NEXTAUTH_URL=${NEXTAUTH_URL:-"https://$SPACE_ID-$SPACE_AUTHOR_NAME.hf.space"}
82
+ export NEXT_TELEMETRY_DISABLED=1
83
+
84
+ echo "Environment initialized successfully"
85
+ }
86
+
87
+ # Main command handling
88
+ case "$1" in
89
+ "create-site")
90
+ create_jekyll_site "$2"
91
+ ;;
92
+ "build-site")
93
+ build_jekyll_site "$2"
94
+ ;;
95
+ "serve-site")
96
+ serve_jekyll_site "$2" "$3"
97
+ ;;
98
+ "start")
99
+ init_environment
100
+ echo "Starting Jekyll Studio on Hugging Face Spaces..."
101
+ echo "Next.js server starting on port $PORT"
102
+ exec npm start -- -p "$PORT"
103
+ ;;
104
+ *)
105
+ echo "Jekyll Studio HF Entrypoint"
106
+ echo "Available commands:"
107
+ echo " create-site <name> - Create new Jekyll site"
108
+ echo " build-site <dir> - Build Jekyll site"
109
+ echo " serve-site <dir> - Serve Jekyll site"
110
+ echo " start - Start Next.js server (default)"
111
+ init_environment
112
+ exec npm start -- -p "${PORT:-7860}"
113
+ ;;
114
+ esac