Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b69bdb424a
|
|||
|
c46a1c0bae
|
|||
|
359916e7d9
|
|||
|
d49b9d48a8
|
|||
|
9caf08a277
|
|||
|
8aabd93273
|
|||
|
6d7987cfae
|
|||
|
abc05ee4e8
|
|||
|
f623ffdd7c
|
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,6 +2,27 @@
|
|||||||
|
|
||||||
Included is a summary of changes to the project, by version. Details can be found in the commit history.
|
Included is a summary of changes to the project, by version. Details can be found in the commit history.
|
||||||
|
|
||||||
|
## v2.0.2
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
* Paths for files in the `pages/` root no longer have an extra `./` in them, which made URLs look ugly and also added an
|
||||||
|
extra blank breadcrumb in the breadcrumbs.
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
* `custom-static` in the instance dir is now ignored and has no special handling --- put static files in `pages/static/`
|
||||||
|
like all the other files that get copied. This also fixes a bug where the build errored if the directory didn't exist.
|
||||||
|
* Some README typos fixed.
|
||||||
|
|
||||||
|
## v2.0.1
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
* The `Image` tag in Markdown files no longer requires the full URL to be specified. Now `Config.BASE_HOST` is
|
||||||
|
prepended to the tag value, which should be the full path to the image.
|
||||||
|
* `.files` are skipped when copying files to the SSG output directory.
|
||||||
|
|
||||||
## v2.0.0
|
## v2.0.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -4,8 +4,7 @@ A lightweight static site generator for Markdown-based sites.
|
|||||||
|
|
||||||
## Installation and Usage
|
## Installation and Usage
|
||||||
|
|
||||||
I recommend getting a release from <https://git.incorporeal.org/bss/incorporeal-cms/releases> and
|
Something like the following should suffice:
|
||||||
installing the Python package in a virtualenv. Something like the following should suffice:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
% virtualenv --python=python3.9 env-py3.9
|
% virtualenv --python=python3.9 env-py3.9
|
||||||
@@ -15,19 +14,19 @@ installing the Python package in a virtualenv. Something like the following shou
|
|||||||
% incorporealcms-build ./path/to/instance ./path/to/output/www/root
|
% incorporealcms-build ./path/to/instance ./path/to/output/www/root
|
||||||
```
|
```
|
||||||
|
|
||||||
This will generate the directory suitable for serving by e.g. Nginx.
|
This will generate the directory suitable for serving by e.g. nginx.
|
||||||
|
|
||||||
## Creating a Site
|
## Creating a Site
|
||||||
|
|
||||||
Put content, notably Markdown content, inside `./your-instance/pages/` and when you are ready, run the build command
|
Put content, notably Markdown content, inside `./your-instance/pages/` and when you are ready, run the build command
|
||||||
above. When you run `incorporealcms-build`, the following happens:
|
above. When you run `incorporealcms-build`, the following happens:
|
||||||
|
|
||||||
* Markdown files (ending in `.md`) are rendered via Python-Markdown as .html files and output to the static site
|
* Markdown files (ending in `.md`) are rendered via Python-Markdown as `.html` files and output to the static site
|
||||||
directory. The `.md` files are also copied there, though this behavior may be toggleable in the future.
|
directory. The `.md` files are also copied there, though this behavior may be toggleable in the future.
|
||||||
* Directory paths (e.g. a request to `/dir/`) can be served via a `/dir/index.md` file, which will generate
|
* Directory paths (e.g. a request to `/dir/`) can be served via a `/dir/index.md` file, which will generate
|
||||||
`/dir/index.html`, with the appropriate web server configuration to use `index.html` for directory listings.
|
`/dir/index.html`, with the appropriate web server configuration to use `index.html` for directory listings.
|
||||||
* Symlinks to files are retained and mirrored into the output directory, and handled per the web server's configuration,
|
* Symlinks to files are retained and mirrored into the output directory, and handled per the web server's configuration,
|
||||||
whanever it is.
|
whatever it is.
|
||||||
* All other files are copied directly, so images, text files, etc., can be referenced naturally as URLs.
|
* All other files are copied directly, so images, text files, etc., can be referenced naturally as URLs.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -53,7 +52,7 @@ out and discuss issues and features and whatnot.
|
|||||||
|
|
||||||
## Author and Licensing
|
## Author and Licensing
|
||||||
|
|
||||||
Written by and copyright (C) 2025 Brian S. Stephan <bss@incorporeal.org>.
|
Written by and copyright (C) 2025 Brian S. Stephan (bss@incorporeal.org).
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ def handle_markdown_file_path(path: str) -> str:
|
|||||||
return template.render(title=page_title,
|
return template.render(title=page_title,
|
||||||
config=Config,
|
config=Config,
|
||||||
description=get_meta_str(md, 'description'),
|
description=get_meta_str(md, 'description'),
|
||||||
image=get_meta_str(md, 'image'),
|
image=Config.BASE_HOST + get_meta_str(md, 'image'),
|
||||||
content=content,
|
content=content,
|
||||||
base_url=Config.BASE_HOST + instance_resource_path_to_request_path(path),
|
base_url=Config.BASE_HOST + instance_resource_path_to_request_path(path),
|
||||||
navs=parent_navs,
|
navs=parent_navs,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
|||||||
SPDX-License-Identifier: GPL-3.0-only
|
SPDX-License-Identifier: GPL-3.0-only
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
@@ -16,6 +17,8 @@ from incorporealcms.error_pages import generate_error_pages
|
|||||||
from incorporealcms.feed import generate_feed
|
from incorporealcms.feed import generate_feed
|
||||||
from incorporealcms.markdown import handle_markdown_file_path
|
from incorporealcms.markdown import handle_markdown_file_path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StaticSiteGenerator(object):
|
class StaticSiteGenerator(object):
|
||||||
"""Generate static site output based on the instance's content."""
|
"""Generate static site output based on the instance's content."""
|
||||||
@@ -53,10 +56,6 @@ class StaticSiteGenerator(object):
|
|||||||
pass
|
pass
|
||||||
self.build_in_destination(program_static_dir, static_output_dir, convert_markdown=False)
|
self.build_in_destination(program_static_dir, static_output_dir, convert_markdown=False)
|
||||||
|
|
||||||
# copy the instance's static dir --- should I deprecate this since it could just be stuff in pages/static/?
|
|
||||||
custom_static_dir = os.path.join(self.instance_dir, 'custom-static')
|
|
||||||
self.build_in_destination(custom_static_dir, static_output_dir, convert_markdown=False)
|
|
||||||
|
|
||||||
# generate the feeds
|
# generate the feeds
|
||||||
cprint("generating feeds", 'green')
|
cprint("generating feeds", 'green')
|
||||||
generate_feed('atom', self.instance_dir, tmp_output_dir)
|
generate_feed('atom', self.instance_dir, tmp_output_dir)
|
||||||
@@ -89,14 +88,19 @@ class StaticSiteGenerator(object):
|
|||||||
cprint(f"copying files from '{source_dir}' to '{dest_dir}'", 'green')
|
cprint(f"copying files from '{source_dir}' to '{dest_dir}'", 'green')
|
||||||
os.chdir(source_dir)
|
os.chdir(source_dir)
|
||||||
for base_dir, subdirs, files in os.walk(source_dir):
|
for base_dir, subdirs, files in os.walk(source_dir):
|
||||||
|
logger.debug("starting to build against %s || %s || %s", base_dir, subdirs, files)
|
||||||
# remove the absolute path of the directory from the base_dir
|
# remove the absolute path of the directory from the base_dir
|
||||||
base_dir = os.path.relpath(base_dir, source_dir)
|
relpath = os.path.relpath(base_dir, source_dir)
|
||||||
|
base_dir = relpath if relpath != '.' else ''
|
||||||
# create subdirs seen here for subsequent depth
|
# create subdirs seen here for subsequent depth
|
||||||
for subdir in subdirs:
|
for subdir in subdirs:
|
||||||
self.build_subdir_in_destination(source_dir, base_dir, subdir, dest_dir)
|
self.build_subdir_in_destination(source_dir, base_dir, subdir, dest_dir)
|
||||||
|
|
||||||
# process and copy files
|
# process and copy files
|
||||||
for file_ in files:
|
for file_ in files:
|
||||||
|
if file_[0] == '.':
|
||||||
|
cprint(f"skipping {file_}", 'yellow')
|
||||||
|
continue
|
||||||
self.build_file_in_destination(source_dir, base_dir, file_, dest_dir, convert_markdown)
|
self.build_file_in_destination(source_dir, base_dir, file_, dest_dir, convert_markdown)
|
||||||
|
|
||||||
def build_subdir_in_destination(self, source_dir: str, base_dir: str, subdir: str, dest_dir: str) -> None:
|
def build_subdir_in_destination(self, source_dir: str, base_dir: str, subdir: str, dest_dir: str) -> None:
|
||||||
@@ -146,6 +150,7 @@ class StaticSiteGenerator(object):
|
|||||||
# render markdown as HTML
|
# render markdown as HTML
|
||||||
if src.endswith('.md') and convert_markdown:
|
if src.endswith('.md') and convert_markdown:
|
||||||
rendered_file = dst.removesuffix('.md') + '.html'
|
rendered_file = dst.removesuffix('.md') + '.html'
|
||||||
|
print(f"rendering file '{src}' -> '{rendered_file}'")
|
||||||
try:
|
try:
|
||||||
content = handle_markdown_file_path(src)
|
content = handle_markdown_file_path(src)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ SPDX-License-Identifier: GPL-3.0-only
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta property="og:url" content="{{ base_url }}">
|
||||||
{% if title %}<meta property="og:title" content="{{ title }}">{% endif %}
|
{% if title %}<meta property="og:title" content="{{ title }}">{% endif %}
|
||||||
{% if description %}<meta property="og:description" content="{{ description }}">{% endif %}
|
{% if description %}<meta property="og:description" content="{{ description }}">{% endif %}
|
||||||
<meta property="og:url" content="{{ base_url }}">
|
|
||||||
{% if image %}<meta property="og:image" content="{{ image }}">{% endif %}
|
{% if image %}<meta property="og:image" content="{{ image }}">{% endif %}
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|||||||
@@ -63,3 +63,31 @@ def test_figures_are_rendered():
|
|||||||
'<span></span></figure>') in data
|
'<span></span></figure>') in data
|
||||||
assert '<figure><img alt="just a logo" src="bss-square-no-bg.png" /></figure>' in data
|
assert '<figure><img alt="just a logo" src="bss-square-no-bg.png" /></figure>' in data
|
||||||
os.chdir(HERE)
|
os.chdir(HERE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_og_image():
|
||||||
|
"""Test that the og:image meta tag is present as expected."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
|
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||||
|
os.chdir(os.path.join(src_dir, 'pages'))
|
||||||
|
|
||||||
|
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'more-metadata.md', tmpdir, True)
|
||||||
|
with open(os.path.join(tmpdir, 'more-metadata.html'), 'r') as markdown_output:
|
||||||
|
data = markdown_output.read()
|
||||||
|
assert ('<meta property="og:image" content="http://example.org/test.img">') in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_og_url():
|
||||||
|
"""Test that the og:url meta tag is present as expected."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
|
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||||
|
os.chdir(os.path.join(src_dir, 'pages'))
|
||||||
|
|
||||||
|
# testing a whole build run because of bugs in how I handle pathing adding a "./" in
|
||||||
|
# the generated URLs for content in the pages/ root
|
||||||
|
ssg.build_in_destination(os.path.join(HERE, 'instance', 'pages'), tmpdir, True)
|
||||||
|
with open(os.path.join(tmpdir, 'index.html'), 'r') as markdown_output:
|
||||||
|
data = markdown_output.read()
|
||||||
|
assert ('<meta property="og:url" content="http://example.org/">') in data
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"handlers": ["console"]
|
"handlers": ["console"]
|
||||||
},
|
},
|
||||||
"incorporealcms.pages": {
|
"incorporealcms.ssg": {
|
||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"handlers": ["console"]
|
"handlers": ["console"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
"""Configure the test application.
|
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
|
||||||
SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
"""
|
|
||||||
|
|
||||||
LOGGING = {
|
|
||||||
'version': 1,
|
|
||||||
'formatters': {
|
|
||||||
'default': {
|
|
||||||
'format': '[%(asctime)s %(levelname)-7s %(name)s] %(message)s',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'handlers': {
|
|
||||||
'console': {
|
|
||||||
'level': 'DEBUG',
|
|
||||||
'class': 'logging.StreamHandler',
|
|
||||||
'formatter': 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'loggers': {
|
|
||||||
'incorporealcms.mdx': {
|
|
||||||
'level': 'DEBUG',
|
|
||||||
'handlers': ['console'],
|
|
||||||
},
|
|
||||||
'incorporealcms.pages': {
|
|
||||||
'level': 'DEBUG',
|
|
||||||
'handlers': ['console'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
1
tests/instance/pages/.ignored-file.md
Normal file
1
tests/instance/pages/.ignored-file.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
this is ignored
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Title: title for the page
|
Title: title for the page
|
||||||
Description: description of this page
|
Description: description of this page
|
||||||
made even longer
|
made even longer
|
||||||
Image: http://buh.com/test.img
|
Image: /test.img
|
||||||
|
|
||||||
hello
|
hello
|
||||||
|
|||||||
@@ -88,3 +88,13 @@ def test_build_in_destination():
|
|||||||
assert os.path.exists(os.path.join(tmpdir, 'index.html'))
|
assert os.path.exists(os.path.join(tmpdir, 'index.html'))
|
||||||
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.md'))
|
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.md'))
|
||||||
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.html'))
|
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.html'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_in_destination_ignores_dot_files():
|
||||||
|
"""Test the ability to walk a source and populate the destination."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
|
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||||
|
generator.build_in_destination(os.path.join(src_dir, 'pages'), tmpdir)
|
||||||
|
|
||||||
|
assert not os.path.exists(os.path.join(tmpdir, '.ignored-file.md'))
|
||||||
|
|||||||
Reference in New Issue
Block a user