Spaces:
Sleeping
Sleeping
Dafa commited on
Upload 7 files
Browse files- Dockerfile +75 -0
- next.config.js +117 -0
- package.json +44 -0
- pages/api/jekyll/build.js +98 -0
- pages/api/jekyll/create.js +141 -0
- pages/api/jekyll/static/[...path].js +100 -0
- usr/local/bin/entrypoint.sh +114 -0
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
|