gpreview
Version:
Preview Ghost themes locally without a full Ghost installation
319 lines ⢠12.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PreviewServer = void 0;
const express_1 = __importDefault(require("express"));
const path_1 = __importDefault(require("path"));
const cors_1 = __importDefault(require("cors"));
const themeLoader_1 = require("../theme/themeLoader");
const handlebarsRenderer_1 = require("../renderer/handlebarsRenderer");
const mockData_1 = require("../data/mockData");
const promises_1 = __importDefault(require("fs/promises"));
const fs_1 = require("fs");
class PreviewServer {
app;
themeLoader;
renderer;
theme;
mockData;
port;
constructor(themePath, options = {}) {
this.app = (0, express_1.default)();
this.themeLoader = new themeLoader_1.ThemeLoader(themePath);
this.renderer = new handlebarsRenderer_1.HandlebarsRenderer();
this.port = options.port || 3000;
this.mockData = mockData_1.defaultMockData;
if (options.dataPath) {
this.loadCustomData(options.dataPath);
}
}
async loadCustomData(dataPath) {
try {
if ((0, fs_1.existsSync)(dataPath)) {
const content = await promises_1.default.readFile(dataPath, 'utf-8');
const customData = JSON.parse(content);
this.mockData = { ...mockData_1.defaultMockData, ...customData };
if (process.env.NODE_ENV !== 'test') {
console.log(`Loaded custom data from ${dataPath}`);
}
}
}
catch (error) {
if (process.env.NODE_ENV !== 'test') {
console.error(`Failed to load custom data: ${error}`);
}
}
}
async initialize() {
this.theme = await this.themeLoader.load();
await this.renderer.initialize(this.theme);
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
}
setupMiddleware() {
this.app.use((0, cors_1.default)());
this.app.use(express_1.default.json());
this.app.use(express_1.default.urlencoded({ extended: true }));
if (this.theme) {
const assetsPath = path_1.default.join(this.theme.themePath, 'assets');
if ((0, fs_1.existsSync)(assetsPath)) {
this.app.use('/assets', express_1.default.static(assetsPath));
}
const cssPath = path_1.default.join(this.theme.themePath, 'assets', 'css');
const jsPath = path_1.default.join(this.theme.themePath, 'assets', 'js');
const imagesPath = path_1.default.join(this.theme.themePath, 'assets', 'images');
if ((0, fs_1.existsSync)(cssPath)) {
this.app.use('/css', express_1.default.static(cssPath));
}
if ((0, fs_1.existsSync)(jsPath)) {
this.app.use('/js', express_1.default.static(jsPath));
}
if ((0, fs_1.existsSync)(imagesPath)) {
this.app.use('/images', express_1.default.static(imagesPath));
}
}
}
setupRoutes() {
this.app.get('/placeholder/:dimensions', (req, res) => {
const { dimensions } = req.params;
const [width, height] = dimensions.split('x').map(Number);
if (!width || !height || width > 2000 || height > 2000) {
return res.status(400).send('Invalid dimensions');
}
const svg = `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text x="50%" y="50%" text-anchor="middle" dy="0.3em"
font-family="Arial, sans-serif" font-size="${Math.min(width, height) / 10}px"
fill="#999">${width}Ć${height}</text>
</svg>
`;
res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Cache-Control', 'public, max-age=31536000');
res.send(svg);
});
this.app.get('/', async (req, res, next) => {
try {
const data = {
site: this.mockData.site,
posts: this.mockData.posts,
tags: this.mockData.tags,
authors: this.mockData.authors,
pagination: {
page: 1,
limit: 10,
pages: Math.ceil((this.mockData.posts?.length || 0) / 10),
total: this.mockData.posts?.length || 0,
},
postFeedStyle: 'Grid',
};
const html = await this.renderer.render('index', data, {
bodyClass: 'home-template',
});
res.send(html);
}
catch (error) {
next(error);
}
});
this.app.get('/post/:slug', async (req, res, next) => {
try {
const { slug } = req.params;
const post = this.mockData.posts?.find((p) => p.slug === slug);
if (!post) {
return this.render404(res);
}
const data = {
site: this.mockData.site,
post,
tags: this.mockData.tags,
authors: this.mockData.authors,
};
const html = await this.renderer.render('post', data, {
bodyClass: 'post-template',
});
res.send(html);
}
catch (error) {
next(error);
}
});
this.app.get('/:slug', async (req, res, next) => {
try {
const { slug } = req.params;
const page = this.mockData.pages?.find((p) => p.slug === slug);
if (page) {
const data = {
site: this.mockData.site,
page,
tags: this.mockData.tags,
authors: this.mockData.authors,
};
const templateName = page.custom_template || 'page';
const html = await this.renderer.render(templateName, data, {
bodyClass: `page-template page-${slug}`,
});
return res.send(html);
}
const post = this.mockData.posts?.find((p) => p.slug === slug);
if (post) {
const data = {
site: this.mockData.site,
post,
tags: this.mockData.tags,
authors: this.mockData.authors,
};
const html = await this.renderer.render('post', data, {
bodyClass: 'post-template',
});
return res.send(html);
}
this.render404(res);
}
catch (error) {
next(error);
}
});
this.app.get('/tag/:slug', async (req, res, next) => {
try {
const { slug } = req.params;
const tag = this.mockData.tags?.find((t) => t.slug === slug);
if (!tag) {
return this.render404(res);
}
const posts = this.mockData.posts?.filter((post) => post.tags.some((t) => t.slug === slug)) || [];
const data = {
site: this.mockData.site,
tag,
posts,
tags: this.mockData.tags,
authors: this.mockData.authors,
pagination: {
page: 1,
limit: 10,
pages: Math.ceil(posts.length / 10),
total: posts.length,
},
};
const html = await this.renderer.render('tag', data, {
bodyClass: `tag-template tag-${slug}`,
});
res.send(html);
}
catch (error) {
next(error);
}
});
this.app.get('/author/:slug', async (req, res, next) => {
try {
const { slug } = req.params;
const author = this.mockData.authors?.find((a) => a.slug === slug);
if (!author) {
return this.render404(res);
}
const posts = this.mockData.posts?.filter((post) => post.authors.some((a) => a.slug === slug)) || [];
const data = {
site: this.mockData.site,
author,
posts,
tags: this.mockData.tags,
authors: this.mockData.authors,
pagination: {
page: 1,
limit: 10,
pages: Math.ceil(posts.length / 10),
total: posts.length,
},
};
const html = await this.renderer.render('author', data, {
bodyClass: `author-template author-${slug}`,
});
res.send(html);
}
catch (error) {
next(error);
}
});
this.app.get('/page/:slug', async (req, res, next) => {
try {
const { slug } = req.params;
const page = this.mockData.pages?.find((p) => p.slug === slug);
if (!page) {
return this.render404(res);
}
const data = {
site: this.mockData.site,
page,
};
const templateName = page.custom_template || 'page';
const html = await this.renderer.render(templateName, data, {
bodyClass: `page-template page-${slug}`,
});
res.send(html);
}
catch (error) {
next(error);
}
});
}
async render404(res) {
try {
const data = {
site: this.mockData.site,
};
const templateName = this.theme?.templates.has('404') ? '404' : 'index';
const html = await this.renderer.render(templateName, data, {
bodyClass: 'error-template error-404',
});
res.status(404).send(html);
}
catch (_error) {
res.status(404).send('<h1>404 - Page Not Found</h1>');
}
}
setupErrorHandling() {
this.app.use((error, req, res, _next) => {
console.error('Server error:', error);
try {
const data = {
site: this.mockData.site,
};
const templateName = this.theme?.templates.has('error') ? 'error' : 'index';
this.renderer.render(templateName, data, {
bodyClass: 'error-template error-500',
}).then(html => {
res.status(500).send(html);
}).catch(() => {
res.status(500).send('<h1>500 - Internal Server Error</h1>');
});
}
catch {
res.status(500).send('<h1>500 - Internal Server Error</h1>');
}
});
}
async start() {
await this.initialize();
this.app.listen(this.port, () => {
console.log(`\\nšØ Ghost Theme Preview Server`);
console.log(`š Theme: ${this.theme?.themePath}`);
console.log(`š Server: http://localhost:${this.port}`);
console.log(`\\nAvailable templates:`);
this.theme?.templates.forEach((_, name) => {
console.log(` - ${name}.hbs`);
});
if (this.theme?.partials.size) {
console.log(`\\nAvailable partials:`);
this.theme?.partials.forEach((_, name) => {
console.log(` - ${name}`);
});
}
console.log('\\nPress Ctrl+C to stop\\n');
});
}
}
exports.PreviewServer = PreviewServer;
//# sourceMappingURL=server.js.map