commit 68b6a87b14598df767435ebae8b1461f41a1d293 Author: CaffeineFueled Date: Thu Oct 9 15:59:47 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19c605c --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environment +venv/ +env/ +ENV/ + +# Output directory (keep structure but ignore contents) +output/* +!output/.gitkeep + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..78f1da5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:24.04 + +# Install Python and pip +RUN apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt /tmp/requirements.txt + +# Install Python dependencies +RUN python3 -m pip install --no-cache-dir -r /tmp/requirements.txt --break-system-packages + +# Copy Python script to /usr/local/bin so it survives volume mounts +COPY picopaper.py /usr/local/bin/picopaper.py + +# Set working directory +WORKDIR /app + +# Add /app to Python path so it can find config.py from mounted volume +ENV PYTHONPATH=/app + +# Run as root to allow writing to mounted volumes +# (The mounted volume will have host user permissions) + +# Generate the site on container start +CMD ["python3", "/usr/local/bin/picopaper.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b146bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..674b16a --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# picopaper + +A minimal static site generator for blogs built with Python 3 and Jinja2 + +- Status: alpha - expect many changes +- Goals: keeping it simple and easy to understand +- Demo: + +Show cases: + +--- + +## Features + +**Available**: +- Simple use, easy to understand and modify +- config file for settings +- Themes +- Long- and short form content +- Pages +- Static files +- separate feeds (used for categories, tagging, etc) `/feed/{tag}` +- exclusion of feeds from main feed (drafts or system notes) + +**Ideas**: +- RSS +- Dark mode +- logo +- custom error pages (404, etc) + +**Not planned**: + +--- + +## Usage + +### Creating a new page or article + +Put markdown file into `items` dir. **Important naming convention**: + +``` +2025-10-03_long_building-a-static-site-generator.md +2025-10-05_short_quick-update_draft.md +``` + +Format: `YYYY-MM-DD_type_slug[_feed].md` + +- `2025-10-03` - date of the article +- `_long_` - type of content: `long`, `short`, or `page` +- `building-a-static-site-generator` - slug/path for the URL +- `_draft` (optional) - feed tag for categorization + +The first `#` header is the title of the article - no frontmatter needed. + +**Types of content**: +- `long` - only title with link to articles will be displayed in feed +- `short` - title and all content will be displayed +- `page` - won't be displayed in feed at all + +### Feeds + +Posts can be tagged with an optional feed category (e.g., `_python`, `_webdev`). Posts with feed tags: +- Appear on the main page (unless excluded in config) +- Have their own feed page at `/feed/{tag}/` + +**Configuration in `config.py`:** +```python +# Exclude specific feeds from main page (they'll still have /feed/name/ pages) +EXCLUDE_FEEDS_FROM_MAIN = ['draft', 'private'] +``` + +This is useful for draft posts or topic-specific content you want separated from the main feed. + +--- + +## Installation + +1. Create and activate virtual environment: +```bash +python3 -m venv venv +source venv/bin/activate # On Linux/Mac +# or: venv\Scripts\activate # On Windows +``` + +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +3. Configure your blog in `config.py`: +```python +BLOG_TITLE = "My Blog" +BLOG_DESCRIPTION = "A simple blog built with picopaper" +THEME = "default" +``` + +4. Generate the site: +```bash +venv/bin/python picopaper.py +``` + +5. Serve locally for testing: +```bash +cd output +python3 -m http.server 8000 +``` + +Visit http://localhost:8000 + +### Docker + +Build and run with Docker: + +```bash +# Build the image +docker build -t picopaper . + +# Run the container +docker run --rm -v $(pwd):/app picopaper +``` + +For Podman (recommended for rootless): + +```bash +# Build the image +podman build -t picopaper . + +# Run with user namespace mapping +podman run --rm --userns=keep-id -v $(pwd):/app picopaper +``` + +The generated site will be available in the `output/` directory. + +--- + +## Directory Structure + +- `items/` - Markdown content files +- `theme/` - Theme directory containing templates and assets + - `default/` - Default theme + - `templates/` - Jinja2 templates + - `assets/` - CSS and static assets +- `images/` - Image files (copied to output) +- `static/` - Static files copied as-is (GPG keys, .well-known, etc.) +- `output/` - Generated site (do not edit) +- `config.py` - Blog configuration + +## Themes + +Themes are organized in the `theme/` directory. Each theme has its own subdirectory containing: +- `templates/` - Jinja2 template files +- `assets/` - CSS, JavaScript, and other static assets + +To switch themes, change the `THEME` setting in `config.py` + +--- + +## Security + +For security concerns or reports, please contact via `hello a t uphillsecurity d o t com` [gpg](https://uphillsecurity.com/gpg). + +--- + +## License + +**Apache License** + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +- ✅ Commercial use +- ✅ Modification +- ✅ Distribution +- ✅ Patent use +- ✅ Private use +- ✅ Limitations +- ❌Trademark use +- ❌Liability +- ❌Warranty diff --git a/config.py b/config.py new file mode 100644 index 0000000..b7976d8 --- /dev/null +++ b/config.py @@ -0,0 +1,15 @@ +"""Configuration file for picopaper blog""" + +BLOG_TITLE = "picopaper" +BLOG_DESCRIPTION = "we like simple." +THEME = "default" + +# Exclude specific feeds from the main page (they'll still have their own /feed/name/ pages) +EXCLUDE_FEEDS_FROM_MAIN = ['draft','private'] # e.g., ['python', 'drafts'] + +# Navigation bar items - list of dictionaries with 'text' and 'url' keys +NAVBAR_ITEMS = [ + {'text': 'Home', 'url': '/'}, + {'text': 'About', 'url': '/about/'} +] + diff --git a/images/.gitkeep b/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/demo-image.jpeg b/images/demo-image.jpeg new file mode 100644 index 0000000..cfce144 Binary files /dev/null and b/images/demo-image.jpeg differ diff --git a/items/2025-10-04_page_about.md b/items/2025-10-04_page_about.md new file mode 100644 index 0000000..fa30bec --- /dev/null +++ b/items/2025-10-04_page_about.md @@ -0,0 +1,3 @@ +# About + +A simple static site generator for your blog or next projects. Please check out the [official documentation](https://git.uphillsecurity.com/cf7/picopaper) for more details. diff --git a/items/2025-10-05_short_hello-world.md b/items/2025-10-05_short_hello-world.md new file mode 100644 index 0000000..cd38159 --- /dev/null +++ b/items/2025-10-05_short_hello-world.md @@ -0,0 +1,5 @@ +# Hello world + +This is a `short` article - the whole content will be displayed in the main feed. + +Is is great for short updates, short-form content and all kind of portfolios. diff --git a/items/2025-10-06_long_your-first-article_tutorial.md b/items/2025-10-06_long_your-first-article_tutorial.md new file mode 100644 index 0000000..7a08a85 --- /dev/null +++ b/items/2025-10-06_long_your-first-article_tutorial.md @@ -0,0 +1,5 @@ +# This is your first long article + +This is a `long` form articles. In the main feed, it is a simple link to the rest of the content. + +Please check the [official documentation](https://git.uphillsecurity.com/cf7/picopaper) for more details. diff --git a/items/2025-10-07_short_another-demo-article_gallery.md b/items/2025-10-07_short_another-demo-article_gallery.md new file mode 100644 index 0000000..2834dec --- /dev/null +++ b/items/2025-10-07_short_another-demo-article_gallery.md @@ -0,0 +1,5 @@ +# Yet another post... + +...with an image! + +![/images/demo-image.jpeg](/images/demo-image.jpeg) diff --git a/items/2025-10-08_short_this-is-a-draft_draft.md b/items/2025-10-08_short_this-is-a-draft_draft.md new file mode 100644 index 0000000..378dff0 --- /dev/null +++ b/items/2025-10-08_short_this-is-a-draft_draft.md @@ -0,0 +1,3 @@ +# Simple draft post + +This post should be hidden in the main feed as it is in the `draft` feed which has been excluded in the `config.py` file. diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/picopaper.py b/picopaper.py new file mode 100644 index 0000000..b7d5407 --- /dev/null +++ b/picopaper.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 + +import os +import re +from datetime import datetime +from pathlib import Path +from jinja2 import Environment, FileSystemLoader +import markdown +from config import BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_FEEDS_FROM_MAIN, NAVBAR_ITEMS + +class SSGGGenerator: + def __init__(self, items_dir='items', output_dir='output', theme=None, blog_title=None, blog_description=None): + self.items_dir = Path(items_dir) + self.output_dir = Path(output_dir) + self.theme = theme or THEME + self.theme_dir = Path('theme') / self.theme + self.templates_dir = self.theme_dir / 'templates' + self.assets_dir = self.theme_dir / 'assets' + self.blog_title = blog_title or BLOG_TITLE + self.blog_description = blog_description or BLOG_DESCRIPTION + self.exclude_feeds = EXCLUDE_FEEDS_FROM_MAIN + self.navbar_items = NAVBAR_ITEMS + + # Setup Jinja2 + self.env = Environment(loader=FileSystemLoader(self.templates_dir)) + + # Setup markdown + self.md = markdown.Markdown(extensions=['extra']) + + def parse_filename(self, filename): + """Parse filename format: YYYY-MM-DD_type_name[_feed].md""" + pattern = r'(\d{4}-\d{2}-\d{2})_(short|long|page)_(.+?)(?:_([a-z0-9-]+))?\.md' + match = re.match(pattern, filename) + + if not match: + return None + + date_str, post_type, name, feed = match.groups() + date = datetime.strptime(date_str, '%Y-%m-%d') + + return { + 'date': date, + 'date_str': date.strftime('%Y-%m-%d'), + 'type': post_type, + 'name': name, + 'feed': feed, + 'filename': filename + } + + def read_post(self, filepath): + """Read markdown file and extract title and content""" + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Extract title (first # heading) + title_match = re.match(r'^#\s+(.+)$', content, re.MULTILINE) + title = title_match.group(1) if title_match else 'Untitled' + + # Remove title from content + if title_match: + content = content[title_match.end():].strip() + + # Convert markdown to HTML + html_content = self.md.convert(content) + + return title, html_content + + def collect_posts(self): + """Collect and parse all posts from items directory""" + posts = [] + + if not self.items_dir.exists(): + print(f"Warning: {self.items_dir} does not exist") + return posts + + for filepath in self.items_dir.glob('*.md'): + parsed = self.parse_filename(filepath.name) + + if not parsed: + print(f"Skipping {filepath.name}: doesn't match naming convention") + continue + + title, content = self.read_post(filepath) + + post = { + 'date': parsed['date_str'], + 'type': parsed['type'], + 'name': parsed['name'], + 'title': title, + 'content': content, + 'slug': parsed['name'], + 'url': f"{parsed['name']}/", + 'feed': parsed['feed'], + 'source': filepath.name + } + + posts.append(post) + + # Sort by date, newest first + posts.sort(key=lambda x: x['date'], reverse=True) + + return posts + + def generate_index(self, posts, feed_name=None): + """Generate index.html with all posts (or feed-specific index)""" + template = self.env.get_template('index.tmpl') + + if feed_name: + title = f"{feed_name} - {self.blog_title}" + output_path = self.output_dir / 'feed' / feed_name / 'index.html' + else: + title = self.blog_title + output_path = self.output_dir / 'index.html' + + html = template.render( + title=title, + blog_title=self.blog_title, + blog_description=self.blog_description, + navbar_items=self.navbar_items, + posts=posts + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html) + + print(f"✓ Generated {output_path}") + + def generate_post_page(self, post): + """Generate individual post page for 'long' posts""" + template = self.env.get_template('post.tmpl') + + html = template.render( + title=f"{post['title']} - {self.blog_title}", + blog_title=self.blog_title, + blog_description=self.blog_description, + navbar_items=self.navbar_items, + post=post + ) + + # Create directory for the post slug + post_dir = self.output_dir / post['slug'] + post_dir.mkdir(exist_ok=True) + + # Generate index.html inside the slug directory + output_path = post_dir / 'index.html' + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html) + + print(f"✓ Generated {output_path}") + + def copy_assets(self): + """Copy theme assets and images to output directory""" + import shutil + + # Copy theme assets + if self.assets_dir.exists(): + dest_dir = self.output_dir / 'assets' + if dest_dir.exists(): + shutil.rmtree(dest_dir) + shutil.copytree(self.assets_dir, dest_dir) + print(f"✓ Copied theme assets to output") + + # Copy images + images_dir = Path('images') + if images_dir.exists(): + dest_dir = self.output_dir / 'images' + if dest_dir.exists(): + shutil.rmtree(dest_dir) + shutil.copytree(images_dir, dest_dir) + print(f"✓ Copied images/ to output") + + # Copy static files (GPG keys, .well-known, etc.) + static_dir = Path('static') + if static_dir.exists(): + for item in static_dir.rglob('*'): + if item.is_file(): + # Preserve directory structure + rel_path = item.relative_to(static_dir) + dest_path = self.output_dir / rel_path + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(item, dest_path) + print(f"✓ Copied static/ to output") + + def generate(self): + """Main generation process""" + print(f"Starting picopaper generation with theme '{self.theme}'...") + + # Create output directory + self.output_dir.mkdir(exist_ok=True) + + # Collect posts + all_posts = self.collect_posts() + print(f"Found {len(all_posts)} posts") + + # Filter out pages and excluded feeds from main feed + feed_posts = [p for p in all_posts + if p['type'] != 'page' + and p['feed'] not in self.exclude_feeds] + + # Generate main index with filtered feed posts + self.generate_index(feed_posts) + + # Group posts by feed (include all posts, not just those in main feed) + feeds = {} + for post in all_posts: + if post['feed'] and post['type'] != 'page': + feeds.setdefault(post['feed'], []).append(post) + + # Generate feed-specific pages + for feed_name, posts in feeds.items(): + self.generate_index(posts, feed_name) + + # Generate individual pages for long posts and pages + for post in all_posts: + if post['type'] in ['long', 'page']: + self.generate_post_page(post) + + # Copy assets + self.copy_assets() + + print(f"\n✓ Site generated successfully in {self.output_dir}/") + +def main(): + generator = SSGGGenerator() + generator.generate() + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ea5dec4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +jinja2>=3.0.0 +markdown>=3.4.0 diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/static/pubkey.asc b/static/pubkey.asc new file mode 100644 index 0000000..279d5f8 --- /dev/null +++ b/static/pubkey.asc @@ -0,0 +1,6 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +Example GPG public key file +This file will be copied as-is to the output directory + +-----END PGP PUBLIC KEY BLOCK----- diff --git a/theme/default/assets/favicon.ico b/theme/default/assets/favicon.ico new file mode 100644 index 0000000..252c877 Binary files /dev/null and b/theme/default/assets/favicon.ico differ diff --git a/theme/default/assets/style.css b/theme/default/assets/style.css new file mode 100644 index 0000000..fb77f82 --- /dev/null +++ b/theme/default/assets/style.css @@ -0,0 +1,92 @@ +body { + max-width: 800px; + margin: 40px auto; + padding: 0 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #333; +} + +header { + border: 1px solid #efefef; + padding: 20px; + margin-bottom: 40px; + border-radius: 10px; +} + +h1 { margin: 0; } + +.blog-description { + margin: 10px 0 0 0; + color: #666; + font-size: 0.9em; +} + +.main-nav { + margin-top: 15px; + font-weight: bold; + display: flex; + gap: 10px; +} + +.nav-item { + padding: 8px 15px; + border: 1px solid #efefef; + border-radius: 10px; + color: #333; + text-decoration: none; + transition: all 0.2s ease; +} + +.nav-item:hover { + background-color: #f5f5f5; + border-color: #0066cc; + color: #0066cc; +} + +a { + color: #0066cc; + text-decoration: none; + font-size: 0.95em; +} + + +.post { + padding: 15px; + margin-top: 10px; + border: 1px solid #efefef; + border-radius: 10px; +} + + +.post-meta { + color: #666; + font-size: 0.9em; + margin-bottom: 10px; +} + +.post-title { + margin: 10px 0; +} + +.post-title a { + color: #333; + text-decoration: none; +} + +.post-title a:hover { + color: #0066cc; +} + +footer { + margin-top: 60px; + padding-top: 20px; + border-top: 1px solid #efefef; + text-align: center; + color: #666; + font-size: 0.9em; +} + +footer p { + font-size: 0.7rem; +} diff --git a/theme/default/templates/content.tmpl b/theme/default/templates/content.tmpl new file mode 100644 index 0000000..e69de29 diff --git a/theme/default/templates/footer.tmpl b/theme/default/templates/footer.tmpl new file mode 100644 index 0000000..58e26f0 --- /dev/null +++ b/theme/default/templates/footer.tmpl @@ -0,0 +1,3 @@ + diff --git a/theme/default/templates/header.tmpl b/theme/default/templates/header.tmpl new file mode 100644 index 0000000..22b854c --- /dev/null +++ b/theme/default/templates/header.tmpl @@ -0,0 +1,9 @@ +
+

{{ blog_title }}

+

{{ blog_description }}

+ +
diff --git a/theme/default/templates/index.tmpl b/theme/default/templates/index.tmpl new file mode 100644 index 0000000..2519d4b --- /dev/null +++ b/theme/default/templates/index.tmpl @@ -0,0 +1,31 @@ + + + + {% include 'meta.tmpl' %} + + + {% include 'header.tmpl' %} + +
+ {% for post in posts %} +
+ +

+ {% if post.type == 'long' %} + {{ post.title }} + {% else %} + {{ post.title }} + {% endif %} +

+ {% if post.type == 'short' %} +
+ {{ post.content | safe }} +
+ {% endif %} +
+ {% endfor %} +
+ + {% include 'footer.tmpl' %} + + diff --git a/theme/default/templates/meta.tmpl b/theme/default/templates/meta.tmpl new file mode 100644 index 0000000..6466300 --- /dev/null +++ b/theme/default/templates/meta.tmpl @@ -0,0 +1,6 @@ + + + +{{ title }} + + diff --git a/theme/default/templates/post.tmpl b/theme/default/templates/post.tmpl new file mode 100644 index 0000000..d1573bd --- /dev/null +++ b/theme/default/templates/post.tmpl @@ -0,0 +1,21 @@ + + + + {% include 'meta.tmpl' %} + + + {% include 'header.tmpl' %} + +
+
+ +

{{ post.title }}

+
+ {{ post.content | safe }} +
+
+
+ + {% include 'footer.tmpl' %} + +