refactor: 完成Handlebars模板系统基础集成
This commit is contained in:
362
src/generator.js
362
src/generator.js
@@ -1,6 +1,113 @@
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const path = require('path');
|
||||
const Handlebars = require('handlebars');
|
||||
|
||||
// 注册Handlebars实例和辅助函数
|
||||
const handlebars = Handlebars.create();
|
||||
|
||||
// 加载和注册Handlebars模板的函数
|
||||
function loadHandlebarsTemplates() {
|
||||
const templatesDir = path.join(process.cwd(), 'templates');
|
||||
|
||||
// 检查基本模板目录是否存在
|
||||
if (!fs.existsSync(templatesDir)) {
|
||||
console.warn('Templates directory not found. Using fallback HTML generation.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 加载布局模板
|
||||
const layoutsDir = path.join(templatesDir, 'layouts');
|
||||
if (fs.existsSync(layoutsDir)) {
|
||||
fs.readdirSync(layoutsDir).forEach(file => {
|
||||
if (file.endsWith('.hbs')) {
|
||||
const layoutName = path.basename(file, '.hbs');
|
||||
const layoutPath = path.join(layoutsDir, file);
|
||||
const layoutContent = fs.readFileSync(layoutPath, 'utf8');
|
||||
handlebars.registerPartial(layoutName, layoutContent);
|
||||
console.log(`Registered layout template: ${layoutName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载组件模板
|
||||
const componentsDir = path.join(templatesDir, 'components');
|
||||
if (fs.existsSync(componentsDir)) {
|
||||
fs.readdirSync(componentsDir).forEach(file => {
|
||||
if (file.endsWith('.hbs')) {
|
||||
const componentName = path.basename(file, '.hbs');
|
||||
const componentPath = path.join(componentsDir, file);
|
||||
const componentContent = fs.readFileSync(componentPath, 'utf8');
|
||||
handlebars.registerPartial(componentName, componentContent);
|
||||
console.log(`Registered component template: ${componentName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 识别并注册默认布局模板
|
||||
const defaultLayoutPath = path.join(layoutsDir, 'default.hbs');
|
||||
if (fs.existsSync(defaultLayoutPath)) {
|
||||
console.log('Default layout template found and registered.');
|
||||
return true;
|
||||
} else {
|
||||
console.warn('Default layout template not found. Using fallback HTML generation.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染Handlebars模板函数
|
||||
function renderTemplate(templateName, data, useLayout = true) {
|
||||
const templatePath = path.join(process.cwd(), 'templates', 'pages', `${templateName}.hbs`);
|
||||
|
||||
// 检查模板是否存在
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
console.warn(`Template ${templateName}.hbs not found. Using fallback HTML generation.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
||||
const template = handlebars.compile(templateContent);
|
||||
|
||||
// 渲染页面内容
|
||||
const pageContent = template(data);
|
||||
|
||||
// 如果不使用布局或者默认布局不存在,直接返回页面内容
|
||||
if (!useLayout) {
|
||||
return pageContent;
|
||||
}
|
||||
|
||||
// 使用布局模板
|
||||
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
||||
if (fs.existsSync(defaultLayoutPath)) {
|
||||
try {
|
||||
// 准备布局数据,包含页面内容
|
||||
const layoutData = {
|
||||
...data,
|
||||
body: pageContent
|
||||
};
|
||||
|
||||
// 加载默认布局模板
|
||||
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
|
||||
const layoutTemplate = handlebars.compile(layoutContent);
|
||||
|
||||
// 渲染完整页面
|
||||
return layoutTemplate(layoutData);
|
||||
} catch (layoutError) {
|
||||
console.error(`Error rendering layout for ${templateName}:`, layoutError);
|
||||
// 如果布局渲染失败,尝试返回页面内容
|
||||
return pageContent;
|
||||
}
|
||||
} else {
|
||||
// 如果找不到布局模板,返回页面内容
|
||||
console.warn('Default layout template not found. Returning page content only.');
|
||||
return pageContent;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error rendering template ${templateName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义函数,防止XSS攻击
|
||||
function escapeHtml(unsafe) {
|
||||
@@ -644,44 +751,73 @@ function processTemplate(template, config) {
|
||||
const googleFontsLink = generateGoogleFontsLink(config);
|
||||
const fontVariables = generateFontVariables(config);
|
||||
|
||||
// 生成所有页面的HTML
|
||||
let allPagesHTML = '';
|
||||
|
||||
// 确保按照导航顺序生成页面
|
||||
if (config.navigation && Array.isArray(config.navigation)) {
|
||||
// 按照导航中的顺序生成页面
|
||||
config.navigation.forEach(navItem => {
|
||||
const pageId = navItem.id;
|
||||
// 如果Handlebars模板系统可用,优先使用Handlebars渲染
|
||||
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
||||
if (fs.existsSync(defaultLayoutPath)) {
|
||||
try {
|
||||
// 准备导航数据,添加submenu字段
|
||||
const navigationData = config.navigation.map(nav => {
|
||||
const navItem = { ...nav };
|
||||
|
||||
// 根据页面ID获取对应的子菜单项(分类)
|
||||
if (nav.id === 'home' && Array.isArray(config.categories)) {
|
||||
navItem.submenu = config.categories;
|
||||
}
|
||||
// 书签页面添加子菜单(分类)
|
||||
else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
|
||||
navItem.submenu = config.bookmarks.categories;
|
||||
}
|
||||
// 项目页面添加子菜单
|
||||
else if (nav.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) {
|
||||
navItem.submenu = config.projects.categories;
|
||||
}
|
||||
// 文章页面添加子菜单
|
||||
else if (nav.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) {
|
||||
navItem.submenu = config.articles.categories;
|
||||
}
|
||||
// 友链页面添加子菜单
|
||||
else if (nav.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) {
|
||||
navItem.submenu = config.friends.categories;
|
||||
}
|
||||
// 通用处理:任意自定义页面的子菜单生成
|
||||
else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) {
|
||||
navItem.submenu = config[nav.id].categories;
|
||||
}
|
||||
|
||||
return navItem;
|
||||
});
|
||||
|
||||
// 跳过搜索结果页
|
||||
if (pageId === 'search-results') {
|
||||
return;
|
||||
}
|
||||
// 准备模板数据
|
||||
const templateData = {
|
||||
site: config.site,
|
||||
navigation: generateNavigation(config.navigation, config),
|
||||
navigationData: navigationData,
|
||||
social: config.social,
|
||||
categories: config.categories,
|
||||
profile: config.profile,
|
||||
googleFontsLink: googleFontsLink,
|
||||
fontVariables: fontVariables,
|
||||
currentYear: currentYear,
|
||||
socialLinks: generateSocialLinks(config.social),
|
||||
searchResults: generateSearchResultsPage(config)
|
||||
};
|
||||
|
||||
let pageContent = '';
|
||||
let isActive = pageId === 'home' ? ' active' : '';
|
||||
// 加载默认布局模板
|
||||
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
|
||||
const layoutTemplate = handlebars.compile(layoutContent);
|
||||
|
||||
// 根据页面ID生成对应内容
|
||||
if (pageId === 'home') {
|
||||
pageContent = generateHomeContent(config);
|
||||
} else if (config[pageId]) {
|
||||
pageContent = generatePageContent(pageId, config[pageId]);
|
||||
} else {
|
||||
pageContent = `<div class="welcome-section">
|
||||
<h2>页面未配置</h2>
|
||||
<p class="subtitle">请配置 ${pageId} 页面</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 添加页面HTML
|
||||
allPagesHTML += `
|
||||
<!-- ${pageId}页 -->
|
||||
<div class="page${isActive}" id="${pageId}">
|
||||
${pageContent}
|
||||
</div>`;
|
||||
});
|
||||
// 渲染模板
|
||||
return layoutTemplate(templateData);
|
||||
} catch (error) {
|
||||
console.error('Error using Handlebars template:', error);
|
||||
console.log('Falling back to placeholder replacement method.');
|
||||
// 出错时回退到原始占位符替换方法
|
||||
}
|
||||
}
|
||||
|
||||
// 生成所有页面的HTML
|
||||
let allPagesHTML = generateAllPagesHTML(config);
|
||||
|
||||
// 创建替换映射
|
||||
const replacements = {
|
||||
'{{SITE_TITLE}}': escapeHtml(config.site.title),
|
||||
@@ -706,6 +842,96 @@ ${pageContent}
|
||||
return processedTemplate;
|
||||
}
|
||||
|
||||
// 生成所有页面的HTML
|
||||
function generateAllPagesHTML(config) {
|
||||
let allPagesHTML = '';
|
||||
|
||||
// 准备Handlebars渲染所需的通用数据
|
||||
const templateData = {
|
||||
site: config.site,
|
||||
navigation: config.navigation,
|
||||
navigationData: config.navigation,
|
||||
social: config.social,
|
||||
categories: config.categories,
|
||||
profile: config.profile,
|
||||
googleFontsLink: generateGoogleFontsLink(config),
|
||||
fontVariables: generateFontVariables(config),
|
||||
currentYear: new Date().getFullYear(),
|
||||
socialLinks: generateSocialLinks(config.social),
|
||||
searchResults: generateSearchResultsPage(config)
|
||||
};
|
||||
|
||||
// 确保按照导航顺序生成页面
|
||||
if (config.navigation && Array.isArray(config.navigation)) {
|
||||
// 按照导航中的顺序生成页面
|
||||
config.navigation.forEach(navItem => {
|
||||
const pageId = navItem.id;
|
||||
|
||||
// 跳过搜索结果页
|
||||
if (pageId === 'search-results') {
|
||||
return;
|
||||
}
|
||||
|
||||
let pageContent = '';
|
||||
let isActive = pageId === 'home' ? ' active' : '';
|
||||
|
||||
// 首先尝试使用模板渲染
|
||||
const pageTemplatePath = path.join(process.cwd(), 'templates', 'pages', `${pageId}.hbs`);
|
||||
if (fs.existsSync(pageTemplatePath)) {
|
||||
// 准备页面特定数据
|
||||
const pageTemplateData = { ...templateData };
|
||||
|
||||
// 添加页面特定数据
|
||||
if (pageId === 'home') {
|
||||
// home页面不需要额外数据,已经包含了categories
|
||||
} else if (config[pageId]) {
|
||||
// 其他页面可能有自己的配置
|
||||
Object.assign(pageTemplateData, config[pageId]);
|
||||
if (config[pageId].categories) {
|
||||
pageTemplateData.categories = config[pageId].categories;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用Handlebars直接编译模板(不使用布局)
|
||||
const templateContent = fs.readFileSync(pageTemplatePath, 'utf8');
|
||||
const template = handlebars.compile(templateContent);
|
||||
pageContent = template(pageTemplateData);
|
||||
console.log(`Rendered ${pageId} page using Handlebars template.`);
|
||||
} catch (error) {
|
||||
console.error(`Error rendering ${pageId} template:`, error);
|
||||
// 回退到原始生成逻辑
|
||||
pageContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果模板渲染失败或模板不存在,使用原始生成逻辑
|
||||
if (!pageContent) {
|
||||
// 根据页面ID生成对应内容
|
||||
if (pageId === 'home') {
|
||||
pageContent = generateHomeContent(config);
|
||||
} else if (config[pageId]) {
|
||||
pageContent = generatePageContent(pageId, config[pageId]);
|
||||
} else {
|
||||
pageContent = `<div class="welcome-section">
|
||||
<h2>页面未配置</h2>
|
||||
<p class="subtitle">请配置 ${pageId} 页面</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加页面HTML
|
||||
allPagesHTML += `
|
||||
<!-- ${pageId}页 -->
|
||||
<div class="page${isActive}" id="${pageId}">
|
||||
${pageContent}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
return allPagesHTML;
|
||||
}
|
||||
|
||||
// 调试函数
|
||||
function debugConfig(config) {
|
||||
console.log('==== DEBUG INFO ====');
|
||||
@@ -731,19 +957,64 @@ function main() {
|
||||
fs.mkdirSync('dist', { recursive: true });
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
const templatePath = 'templates/index.html';
|
||||
// 初始化Handlebars模板系统
|
||||
const handlebarsAvailable = loadHandlebarsTemplates();
|
||||
|
||||
// 准备Handlebars渲染所需的通用数据
|
||||
const templateData = {
|
||||
site: config.site,
|
||||
navigation: config.navigation,
|
||||
navigationData: config.navigation,
|
||||
social: config.social,
|
||||
categories: config.categories,
|
||||
profile: config.profile,
|
||||
googleFontsLink: generateGoogleFontsLink(config),
|
||||
fontVariables: generateFontVariables(config),
|
||||
currentYear: new Date().getFullYear(),
|
||||
socialLinks: generateSocialLinks(config.social),
|
||||
searchResults: generateSearchResultsPage(config)
|
||||
};
|
||||
|
||||
let htmlContent = '';
|
||||
|
||||
if (fs.existsSync(templatePath)) {
|
||||
// 读取模板并处理
|
||||
const template = fs.readFileSync(templatePath, 'utf8');
|
||||
htmlContent = processTemplate(template, config);
|
||||
console.log(`Using template from ${templatePath} and injecting content`);
|
||||
// 尝试使用Handlebars模板渲染
|
||||
if (handlebarsAvailable) {
|
||||
console.log('Handlebars templates are available.');
|
||||
|
||||
// 渲染逻辑:先尝试使用页面模板和默认布局渲染
|
||||
const renderedContent = renderTemplate('home', templateData);
|
||||
|
||||
if (renderedContent) {
|
||||
// 使用模板成功渲染
|
||||
htmlContent = renderedContent;
|
||||
console.log('Successfully rendered using Handlebars templates.');
|
||||
} else {
|
||||
// 模板渲染失败,回退到传统模板处理
|
||||
console.log('Failed to render with Handlebars templates, using traditional template processing.');
|
||||
|
||||
const templatePath = 'templates/index.html';
|
||||
if (fs.existsSync(templatePath)) {
|
||||
const template = fs.readFileSync(templatePath, 'utf8');
|
||||
htmlContent = processTemplate(template, config);
|
||||
} else {
|
||||
// 如果没有任何模板,使用纯生成的HTML
|
||||
htmlContent = generateHTML(config);
|
||||
console.log('No template files found, using generated HTML.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有模板文件,使用生成的HTML
|
||||
htmlContent = generateHTML(config);
|
||||
console.log('No template file found, using generated HTML');
|
||||
// Handlebars不可用,使用传统模板处理
|
||||
console.log('Handlebars templates are not available, using traditional template processing.');
|
||||
|
||||
const templatePath = 'templates/index.html';
|
||||
if (fs.existsSync(templatePath)) {
|
||||
const template = fs.readFileSync(templatePath, 'utf8');
|
||||
htmlContent = processTemplate(template, config);
|
||||
} else {
|
||||
// 如果没有任何模板,使用纯生成的HTML
|
||||
htmlContent = generateHTML(config);
|
||||
console.log('No template files found, using generated HTML.');
|
||||
}
|
||||
}
|
||||
|
||||
// 生成HTML
|
||||
@@ -766,5 +1037,8 @@ module.exports = {
|
||||
generateHTML,
|
||||
copyStaticFiles,
|
||||
generateNavigation,
|
||||
generateCategories
|
||||
generateCategories,
|
||||
loadHandlebarsTemplates,
|
||||
renderTemplate,
|
||||
generateAllPagesHTML
|
||||
};
|
||||
|
||||
12
templates/components/category.hbs
Normal file
12
templates/components/category.hbs
Normal file
@@ -0,0 +1,12 @@
|
||||
<section class="category" id="{{name}}">
|
||||
<h2><i class="{{icon}}"></i> {{name}}</h2>
|
||||
<div class="sites-grid">
|
||||
{{#if sites.length}}
|
||||
{{#each sites}}
|
||||
{{> site-card}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<p class="empty-sites">暂无网站</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
21
templates/components/navigation.hbs
Normal file
21
templates/components/navigation.hbs
Normal file
@@ -0,0 +1,21 @@
|
||||
{{#each this}}
|
||||
<div class="nav-item-wrapper">
|
||||
<a href="#" class="nav-item{{#if active}} active{{/if}}" data-page="{{id}}">
|
||||
<div class="icon-container">
|
||||
<i class="{{icon}}"></i>
|
||||
</div>
|
||||
<span class="nav-text">{{name}}</span>
|
||||
{{#if submenu}}<i class="fas fa-chevron-down submenu-toggle"></i>{{/if}}
|
||||
</a>
|
||||
{{#if submenu}}
|
||||
<div class="submenu">
|
||||
{{#each submenu}}
|
||||
<a href="#{{name}}" class="submenu-item" data-page="{{../id}}" data-category="{{name}}">
|
||||
<i class="{{icon}}"></i>
|
||||
<span>{{name}}</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
7
templates/components/site-card.hbs
Normal file
7
templates/components/site-card.hbs
Normal file
@@ -0,0 +1,7 @@
|
||||
{{#if url}}
|
||||
<a href="{{url}}" class="site-card" title="{{name}} - {{description}}">
|
||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
||||
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
||||
<p>{{description}}</p>
|
||||
</a>
|
||||
{{/if}}
|
||||
119
templates/layouts/default.hbs
Normal file
119
templates/layouts/default.hbs
Normal file
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{site.title}}</title>
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
{{{googleFontsLink}}}
|
||||
<style>
|
||||
{{{fontVariables}}}
|
||||
</style>
|
||||
<!-- 预设主题和侧边栏状态,避免闪烁 -->
|
||||
<script>
|
||||
(function() {
|
||||
// 读取并应用主题设置
|
||||
var savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'light') {
|
||||
document.documentElement.classList.add('theme-preload');
|
||||
}
|
||||
|
||||
// 读取并应用侧边栏状态
|
||||
var sidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
if (sidebarCollapsed && !isMobile) {
|
||||
document.documentElement.classList.add('sidebar-collapsed-preload');
|
||||
}
|
||||
|
||||
// 添加这个类用于控制初始渲染
|
||||
document.documentElement.classList.add('preload');
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="loading">
|
||||
<!-- 滚动进度指示条 -->
|
||||
<div class="scroll-progress"></div>
|
||||
<div class="layout">
|
||||
<!-- 移动端按钮 -->
|
||||
<div class="mobile-buttons">
|
||||
<button class="menu-toggle" aria-label="切换菜单">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<button class="search-toggle" aria-label="切换搜索">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="overlay"></div>
|
||||
|
||||
<!-- 左侧导航 -->
|
||||
<nav class="sidebar">
|
||||
<div class="logo">
|
||||
<h1>{{site.logo_text}}</h1>
|
||||
<button class="sidebar-toggle" aria-label="收起/展开侧边栏">
|
||||
<i class="fas fa-chevron-left toggle-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div class="nav-section">
|
||||
{{#if navigationData}}
|
||||
{{> navigation navigationData}}
|
||||
{{else}}
|
||||
{{{navigation}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
{{{socialLinks}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
<p>© {{currentYear}} <a href="https://github.com/rbetree/menav" target="_blank" rel="noopener">MeNav</a></p>
|
||||
<p>by <a href="https://github.com/rbetree" target="_blank" rel="noopener">rbetree</a></p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<main class="content">
|
||||
<!-- 搜索框容器 -->
|
||||
<div class="search-container">
|
||||
<div class="search-box">
|
||||
<input type="text" id="search" placeholder="搜索...">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{{body}}}
|
||||
|
||||
<!-- 搜索结果页 -->
|
||||
<div class="page" id="search-results">
|
||||
<div class="welcome-section">
|
||||
<h2>搜索结果</h2>
|
||||
<p class="subtitle">在所有页面中找到的匹配项</p>
|
||||
</div>
|
||||
{{#each navigation}}
|
||||
<section class="category search-section" data-section="{{id}}" style="display: none;">
|
||||
<h2><i class="{{icon}}"></i> {{name}}匹配项</h2>
|
||||
<div class="sites-grid"></div>
|
||||
</section>
|
||||
{{/each}}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
<button class="theme-toggle" aria-label="切换主题">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
10
templates/pages/home.hbs
Normal file
10
templates/pages/home.hbs
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="page active" id="home">
|
||||
<div class="welcome-section">
|
||||
<h2>{{profile.title}}</h2>
|
||||
<h3>{{profile.subtitle}}</h3>
|
||||
<p class="subtitle">{{profile.description}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
</div>
|
||||
Reference in New Issue
Block a user