Files
log/src/commands/init.rs
2025-06-16 22:09:04 +09:00

886 lines
20 KiB
Rust

use anyhow::Result;
use colored::Colorize;
use std::fs;
use std::path::PathBuf;
pub async fn execute(path: PathBuf) -> Result<()> {
println!("{}", "Initializing new blog...".green());
// Create directory structure
let dirs = vec![
"content",
"content/posts",
"templates",
"static",
"static/css",
"static/js",
"static/images",
"public",
];
for dir in dirs {
let dir_path = path.join(dir);
fs::create_dir_all(&dir_path)?;
println!(" {} {}", "Created".cyan(), dir_path.display());
}
// Create default config
let config_content = r#"[site]
title = "My Blog"
description = "A blog powered by ailog"
base_url = "https://example.com"
language = "ja"
author = "Your Name"
[build]
highlight_code = true
minify = false
[ai]
enabled = true
auto_translate = false
comment_moderation = false
ask_ai = true
provider = "ollama"
model = "gemma3:4b"
host = "https://ollama.syui.ai"
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
handle = "ai.syui.ai"
[oauth]
json = "client-metadata.json"
redirect = "oauth/callback"
admin = "ai.syui.ai"
collection = "ai.syui.log"
pds = "syu.is"
handle_list = ["syui.syui.ai", "yui.syui.ai", "ai.syui.ai", "syui.syu.is", "ai.syu.is"]
"#;
fs::write(path.join("config.toml"), config_content)?;
println!(" {} config.toml", "Created".cyan());
// Create modern template
let base_template = r#"<!DOCTYPE html>
<html lang="{{ config.language }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.title }}{% endblock %}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<header class="main-header">
<div class="header-content">
<h1><a href="/" class="site-title">{{ config.title }}</a></h1>
<div class="header-actions">
<button class="ask-ai-btn" onclick="toggleAskAI()">
<span class="ai-icon">🤖</span>
Ask AI
</button>
</div>
</div>
</header>
<div class="ask-ai-panel" id="askAiPanel" style="display: none;">
<div class="ask-ai-content">
<h3>Hi! 👋</h3>
<p>I'm an AI assistant trained on this blog's content.</p>
<p>Ask me anything about the articles here.</p>
<div class="ask-ai-form">
<input type="text" id="aiQuestion" placeholder="What would you like to know?" />
<button onclick="askQuestion()">Ask</button>
</div>
<div id="aiResponse" class="ai-response"></div>
</div>
</div>
<main class="main-content">
{% block content %}{% endblock %}
</main>
{% block sidebar %}{% endblock %}
</div>
<footer class="main-footer">
<p>&copy; {{ config.author | default(value=config.title) }}</p>
</footer>
<script>
function toggleAskAI() {
const panel = document.getElementById('askAiPanel');
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
document.getElementById('aiQuestion').focus();
}
}
async function askQuestion() {
const question = document.getElementById('aiQuestion').value;
const responseDiv = document.getElementById('aiResponse');
if (!question.trim()) return;
responseDiv.innerHTML = '<div class="loading">Thinking...</div>';
try {
const response = await fetch('/api/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ question: question })
});
const data = await response.json();
responseDiv.innerHTML = `<div class="ai-answer">${data.answer}</div>`;
} catch (error) {
responseDiv.innerHTML = '<div class="error">Sorry, I encountered an error. Please try again.</div>';
}
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.getElementById('askAiPanel').style.display = 'none';
}
});
</script>
</body>
</html>"#;
fs::write(path.join("templates/base.html"), base_template)?;
println!(" {} templates/base.html", "Created".cyan());
let index_template = r#"{% extends "base.html" %}
{% block content %}
<div class="timeline-container">
<div class="timeline-header">
<h2>Timeline</h2>
</div>
<div class="timeline-feed">
{% for post in posts %}
<article class="timeline-post">
<div class="post-header">
<div class="post-meta">
<time class="post-date">{{ post.date }}</time>
{% if post.language %}
<span class="post-lang">{{ post.language }}</span>
{% endif %}
</div>
</div>
<div class="post-content">
<h3 class="post-title">
<a href="{{ post.url }}">{{ post.title }}</a>
</h3>
{% if post.excerpt %}
<p class="post-excerpt">{{ post.excerpt }}</p>
{% endif %}
<div class="post-actions">
<a href="{{ post.url }}" class="read-more">Read more</a>
{% if post.markdown_url %}
<a href="{{ post.markdown_url }}" class="view-markdown" title="View Markdown">📝</a>
{% endif %}
{% if post.translation_url %}
<a href="{{ post.translation_url }}" class="view-translation" title="View Translation">🌐</a>
{% endif %}
</div>
</div>
</article>
{% endfor %}
</div>
{% if posts|length == 0 %}
<div class="empty-state">
<p>No posts yet. Start writing!</p>
</div>
{% endif %}
</div>
{% endblock %}"#;
fs::write(path.join("templates/index.html"), index_template)?;
println!(" {} templates/index.html", "Created".cyan());
let post_template = r#"{% extends "base.html" %}
{% block title %}{{ post.title }} - {{ config.title }}{% endblock %}
{% block content %}
<div class="article-container">
<article class="article-content">
<header class="article-header">
<h1 class="article-title">{{ post.title }}</h1>
<div class="article-meta">
<time class="article-date">{{ post.date }}</time>
{% if post.language %}
<span class="article-lang">{{ post.language }}</span>
{% endif %}
</div>
<div class="article-actions">
{% if post.markdown_url %}
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
📝 Markdown
</a>
{% endif %}
{% if post.translation_url %}
<a href="{{ post.translation_url }}" class="action-btn translation-btn" title="View Translation">
🌐 {% if post.language == 'ja' %}English{% else %}日本語{% endif %}
</a>
{% endif %}
</div>
</header>
<div class="article-body">
{{ post.content | safe }}
</div>
</article>
</div>
{% endblock %}
{% block sidebar %}
<aside class="article-sidebar">
<nav class="toc">
<h3>Contents</h3>
<div id="toc-content">
<!-- TOC will be generated by JavaScript -->
</div>
</nav>
</aside>
<script>
document.addEventListener('DOMContentLoaded', function() {
generateTableOfContents();
});
function generateTableOfContents() {
const tocContainer = document.getElementById('toc-content');
const headings = document.querySelectorAll('.article-body h1, .article-body h2, .article-body h3, .article-body h4, .article-body h5, .article-body h6');
if (headings.length === 0) {
tocContainer.innerHTML = '<p class="no-toc">No headings found</p>';
return;
}
const tocList = document.createElement('ul');
tocList.className = 'toc-list';
headings.forEach((heading, index) => {
const id = `heading-${index}`;
heading.id = id;
const listItem = document.createElement('li');
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
const link = document.createElement('a');
link.href = `#${id}`;
link.textContent = heading.textContent;
link.className = 'toc-link';
// Smooth scroll behavior
link.addEventListener('click', function(e) {
e.preventDefault();
heading.scrollIntoView({ behavior: 'smooth' });
});
listItem.appendChild(link);
tocList.appendChild(listItem);
});
tocContainer.appendChild(tocList);
}
</script>
{% endblock %}"#;
fs::write(path.join("templates/post.html"), post_template)?;
println!(" {} templates/post.html", "Created".cyan());
// Create modern CSS
let css_content = r#"/* Base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #1f2328;
background-color: #ffffff;
font-size: 16px;
}
.container {
min-height: 100vh;
display: grid;
grid-template-rows: auto auto 1fr auto;
grid-template-areas:
"header"
"ask-ai"
"main"
"footer";
}
/* Header styles */
.main-header {
grid-area: header;
background: #ffffff;
border-bottom: 1px solid #d1d9e0;
padding: 16px 24px;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.site-title {
color: #1f2328;
text-decoration: none;
font-size: 20px;
font-weight: 600;
}
.site-title:hover {
color: #0969da;
}
/* Ask AI styles */
.ask-ai-btn {
background: #0969da;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: background-color 0.2s;
}
.ask-ai-btn:hover {
background: #0860ca;
}
.ai-icon {
font-size: 16px;
}
.ask-ai-panel {
grid-area: ask-ai;
background: #f6f8fa;
border-bottom: 1px solid #d1d9e0;
padding: 24px;
}
.ask-ai-content {
max-width: 1200px;
margin: 0 auto;
}
.ask-ai-content h3 {
color: #1f2328;
margin-bottom: 8px;
}
.ask-ai-content p {
color: #656d76;
margin-bottom: 16px;
}
.ask-ai-form {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.ask-ai-form input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d1d9e0;
border-radius: 6px;
font-size: 14px;
}
.ask-ai-form button {
background: #0969da;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}
.ai-response {
background: white;
border: 1px solid #d1d9e0;
border-radius: 6px;
padding: 16px;
margin-top: 16px;
min-height: 60px;
}
.loading {
color: #656d76;
font-style: italic;
}
.ai-answer {
color: #1f2328;
line-height: 1.5;
}
.error {
color: #d1242f;
}
/* Main content styles */
.main-content {
grid-area: main;
max-width: 1200px;
margin: 0 auto;
padding: 24px;
width: 100%;
}
/* Timeline styles */
.timeline-container {
max-width: 600px;
margin: 0 auto;
}
.timeline-header {
margin-bottom: 24px;
text-align: center;
}
.timeline-header h2 {
color: #1f2328;
font-size: 24px;
font-weight: 600;
}
.timeline-feed {
display: flex;
flex-direction: column;
gap: 24px;
}
.timeline-post {
background: #ffffff;
border: 1px solid #d1d9e0;
border-radius: 8px;
padding: 20px;
transition: box-shadow 0.2s;
}
.timeline-post:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-header {
margin-bottom: 12px;
}
.post-meta {
display: flex;
gap: 12px;
align-items: center;
}
.post-date {
color: #656d76;
font-size: 14px;
}
.post-lang {
background: #f6f8fa;
color: #656d76;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.post-title {
margin-bottom: 8px;
}
.post-title a {
color: #1f2328;
text-decoration: none;
font-size: 18px;
font-weight: 600;
}
.post-title a:hover {
color: #0969da;
}
.post-excerpt {
color: #656d76;
margin-bottom: 16px;
line-height: 1.5;
}
.post-actions {
display: flex;
gap: 16px;
align-items: center;
}
.read-more {
color: #0969da;
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.read-more:hover {
text-decoration: underline;
}
.view-markdown, .view-translation {
color: #656d76;
text-decoration: none;
font-size: 14px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.view-markdown:hover, .view-translation:hover {
background: #f6f8fa;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #656d76;
}
/* Article page styles */
.article-container {
display: grid;
grid-template-columns: 1fr 240px;
gap: 40px;
max-width: 1200px;
margin: 0 auto;
}
.article-content {
min-width: 0;
}
.article-header {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #d1d9e0;
}
.article-title {
color: #1f2328;
font-size: 32px;
font-weight: 600;
margin-bottom: 16px;
line-height: 1.25;
}
.article-meta {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 16px;
}
.article-date {
color: #656d76;
font-size: 14px;
}
.article-lang {
background: #f6f8fa;
color: #656d76;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.article-actions {
display: flex;
gap: 12px;
}
.action-btn {
color: #0969da;
text-decoration: none;
font-size: 14px;
padding: 6px 12px;
border: 1px solid #d1d9e0;
border-radius: 6px;
transition: all 0.2s;
}
.action-btn:hover {
background: #f6f8fa;
border-color: #0969da;
}
/* Article content */
.article-body {
color: #1f2328;
line-height: 1.6;
}
.article-body h1,
.article-body h2,
.article-body h3,
.article-body h4,
.article-body h5,
.article-body h6 {
color: #1f2328;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.article-body h1 { font-size: 32px; }
.article-body h2 { font-size: 24px; }
.article-body h3 { font-size: 20px; }
.article-body h4 { font-size: 16px; }
.article-body p {
margin-bottom: 16px;
}
.article-body ul,
.article-body ol {
margin-bottom: 16px;
padding-left: 24px;
}
.article-body li {
margin-bottom: 4px;
}
.article-body blockquote {
border-left: 4px solid #d1d9e0;
padding-left: 16px;
margin: 16px 0;
color: #656d76;
}
.article-body pre {
background: #f6f8fa;
border: 1px solid #d1d9e0;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
margin: 16px 0;
font-size: 14px;
}
.article-body code {
background: #f6f8fa;
padding: 2px 4px;
border-radius: 4px;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
font-size: 14px;
}
.article-body pre code {
background: none;
padding: 0;
}
/* Sidebar styles */
.article-sidebar {
position: sticky;
top: 100px;
height: fit-content;
}
.toc {
background: #f6f8fa;
border: 1px solid #d1d9e0;
border-radius: 8px;
padding: 16px;
}
.toc h3 {
color: #1f2328;
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.toc-list {
list-style: none;
}
.toc-item {
margin-bottom: 8px;
}
.toc-link {
color: #656d76;
text-decoration: none;
font-size: 14px;
line-height: 1.4;
display: block;
padding: 4px 0;
transition: color 0.2s;
}
.toc-link:hover {
color: #0969da;
}
.toc-h1 { padding-left: 0; }
.toc-h2 { padding-left: 12px; }
.toc-h3 { padding-left: 24px; }
.toc-h4 { padding-left: 36px; }
.toc-h5 { padding-left: 48px; }
.toc-h6 { padding-left: 60px; }
.no-toc {
color: #656d76;
font-size: 14px;
font-style: italic;
}
/* Footer styles */
.main-footer {
grid-area: footer;
background: #f6f8fa;
border-top: 1px solid #d1d9e0;
padding: 24px;
text-align: center;
}
.main-footer p {
color: #656d76;
font-size: 14px;
}
/* Responsive design */
@media (max-width: 1024px) {
.article-container {
grid-template-columns: 1fr;
gap: 24px;
}
.article-sidebar {
position: static;
order: -1;
}
}
@media (max-width: 768px) {
.main-header {
padding: 12px 16px;
}
.header-content {
gap: 16px;
}
.ask-ai-panel {
padding: 16px;
}
.ask-ai-form {
flex-direction: column;
}
.timeline-container {
max-width: 100%;
}
.timeline-post {
padding: 16px;
}
.article-title {
font-size: 24px;
}
.article-actions {
flex-wrap: wrap;
}
.main-content {
padding: 16px;
}
}"#;
fs::write(path.join("static/css/style.css"), css_content)?;
println!(" {} static/css/style.css", "Created".cyan());
// Create sample post
let sample_post = r#"---
title: "Welcome to ailog"
date: 2025-01-06
tags: ["welcome", "ailog"]
---
# Welcome to ailog
This is your first post powered by **ailog** - a static blog generator with AI features.
## Features
- Fast static site generation
- Markdown support with frontmatter
- AI-powered features (coming soon)
- atproto integration for comments
## Getting Started
Create new posts with:
```bash
ailog new "My New Post"
```
Build your blog with:
```bash
ailog build
```
Happy blogging!"#;
fs::write(path.join("content/posts/welcome.md"), sample_post)?;
println!(" {} content/posts/welcome.md", "Created".cyan());
println!("\n{}", "Blog initialized successfully!".green().bold());
println!("\nNext steps:");
println!(" 1. {} {}", "cd".yellow(), path.display());
println!(" 2. {} build", "ailog".yellow());
println!(" 3. {} serve", "ailog".yellow());
println!("\nOr use path as argument:");
println!(" {} -- build {}", "cargo run".yellow(), path.display());
println!(" {} -- serve {}", "cargo run".yellow(), path.display());
println!("\nTo create a new post:");
println!(" {} -- new \"Post Title\" {}", "cargo run".yellow(), path.display());
Ok(())
}