diff --git a/.github/workflows/build-preview-site.yml b/.github/workflows/build-preview-site.yml
new file mode 100644
index 00000000..2639c053
--- /dev/null
+++ b/.github/workflows/build-preview-site.yml
@@ -0,0 +1,76 @@
+name: Build pelican-themes preview site
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+on:
+ # Triggers the workflow on push or pull request events but only for the "main" branch
+ push:
+ branches: [ "master" ]
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 1
+ submodules: recursive
+ - name: Checkout pelican
+ uses: actions/checkout@v3
+ with:
+ repository: getpelican/pelican
+ path: _pelican
+ fetch-depth: 1
+ - name: Setup python-3.10
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.10"
+ - name: Cache Playwright browsers
+ uses: actions/cache@v3
+ with:
+ path: ~/.cache/ms-playwright/
+ key: ${{ runner.os }}-browsers
+ - name: Install pelican and shot-scraper
+ run: pip install pelican[markdown] shot-scraper
+ - name: Setup shot-scraper
+ run: shot-scraper install
+ - name: Generate output
+ run: python build-theme-previews.py
+
+ # Rsync to server hosting pelicanthemes.com
+ - name: Install SSH key
+ uses: shimataro/ssh-key-action@v2
+ with:
+ key: ${{ secrets.SSH_PRIVATE_KEY }}
+ known_hosts: unnecessary
+ - name: Adding known hosts
+ run: ssh-keyscan -p 22 -H pelicanthemes.com >> ~/.ssh/known_hosts
+ - name: Deploy with rsync
+ run: rsync -avz ./_output/ deploy@pelicanthemes.com:~/roles/caddy/sites/pelicanthemes.com/
+
+ # Deploy to Github Pages
+ - name: Setup Pages
+ uses: actions/configure-pages@v2
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v1
+ with:
+ path: '_output'
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v1
diff --git a/.gitignore b/.gitignore
index f791bb41..e79f6b5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,8 @@ Thumbs.db
*~
.swp
.*.swp
+
+# Preview artifacts #
+#####################
+_pelican
+_output
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index fcd145b5..4d63e79f 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -143,9 +143,6 @@
[submodule "alchemy"]
path = alchemy
url = https://github.com/nairobilug/pelican-alchemy.git
-[submodule "Nuja"]
- path = Nuja
- url = https://github.com/allenskd/Nuja.git
[submodule "pjport"]
path = pjport
url = https://github.com/xm3ron/pjport.git
diff --git a/Nuja b/Nuja
deleted file mode 160000
index a58fd614..00000000
--- a/Nuja
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a58fd6149d83c1b262a75cac785b51e8e6fb0dd9
diff --git a/build-theme-previews.py b/build-theme-previews.py
new file mode 100644
index 00000000..fa766f8a
--- /dev/null
+++ b/build-theme-previews.py
@@ -0,0 +1,237 @@
+import argparse
+import logging
+import subprocess
+import os
+
+from rich.logging import RichHandler
+from rich.console import Console
+
+
+FORMAT = "%(message)s"
+logging.basicConfig(
+ level="NOTSET",
+ format=FORMAT,
+ datefmt="[%X]",
+ handlers=[RichHandler(show_path=False, console=Console(force_terminal=True))]
+)
+logger = logging.getLogger()
+
+HTML_HEADER = """\
+
+
+
+
+
+
+pelican-themes Preview
+"""
+
+HTML_FOOTER = """\
+
+
+
+
+"""
+
+
+def setup_folders(args):
+ theme_root = os.path.abspath(os.path.dirname(__file__))
+ output_root = os.path.abspath(os.path.join(theme_root, args.output))
+ samples_root = os.path.abspath(os.path.join(theme_root, args.samples))
+ screenshot_root = os.path.abspath(os.path.join(output_root, "_screenshots"))
+
+ # requires `getpelican/pelican` cloned in `_pelican` folder
+ if os.path.exists(samples_root):
+ os.makedirs(os.path.join(samples_root, "content", "images"), exist_ok=True) # silence warning
+ else:
+ raise RuntimeError(
+ f"Samples folder does not exist: {samples_root}. "
+ "You can use `samples` from pelican by cloning it to `_pelican` folder"
+ )
+ # create output and screenshot folders
+ os.makedirs(output_root, exist_ok=True)
+ os.makedirs(screenshot_root, exist_ok=True)
+
+ return theme_root, samples_root, output_root, screenshot_root
+
+
+def build_theme_previews(theme_root, samples_root, output_root, screenshot_root):
+ themes = [item for item in os.listdir(theme_root) if os.path.isdir(item) and not item.startswith((".", "_"))]
+ logger.info(f"processing {len(themes)} themes...")
+
+ # launch web server for taking screenshots
+ server = subprocess.Popen(
+ ["python", "-m", "http.server", "-d", output_root],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+
+ fail = {}
+ success = {}
+ screenshot_processes = []
+
+ for theme in sorted(themes, key=lambda x: x.lower()):
+ theme_path = os.path.join(theme_root, theme)
+ if os.path.exists(os.path.join(theme_path, theme, "templates")):
+ # actual theme is in a subfolder
+ theme_path = os.path.join(theme_path, theme)
+ output_path = os.path.join(output_root, theme)
+ try:
+ process = subprocess.run([
+ "pelican",
+ os.path.join(samples_root, "content"),
+ "--settings", os.path.join(samples_root, "pelican.conf.py"),
+ "--extra-settings", f"SITENAME=\"{theme} preview\"",
+ "--relative-urls",
+ "--theme-path", theme_path,
+ "--output", output_path,
+ "--ignore-cache",
+ "--delete-output-directory"
+ ],
+ check=True, capture_output=True, universal_newlines=True)
+ except subprocess.CalledProcessError as exc:
+ logger.error(f"[red]failed to generate : {theme}[/]", extra={"markup": True})
+ fail[theme] = exc.stdout
+ continue
+ success[theme] = output_path
+ screenshot_path = os.path.join(screenshot_root, f"{theme}.png")
+ screenshot_processes.append(
+ subprocess.Popen(
+ ["shot-scraper", f"http://localhost:8000/{theme}", "-o", screenshot_path, "-w", "1280", "-h", "780", "--wait", "1000"],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+ )
+ logger.info(f"[green]successfully generated : {theme}[/]", extra={"markup": True})
+
+ # cleanup
+ logger.info("finalizing screenshots...")
+ for process in screenshot_processes:
+ process.wait()
+ server.terminate()
+ return success, fail
+
+
+def write_index_files(output_root, success, fail):
+ logger.info("generating index files...")
+ with open(os.path.join(output_root, "index.html"), "w") as outfile:
+ outfile.write(HTML_HEADER)
+ for theme, theme_path in sorted(success.items(), key=lambda x: x[0].lower()):
+ outfile.write(f'{theme}
')
+ outfile.write(HTML_FOOTER.format(success=len(success), fail=len(fail)))
+
+ with open(os.path.join(output_root, "failed.html"), "w") as outfile:
+ outfile.write(HTML_HEADER)
+ for theme, reason in sorted(fail.items(), key=lambda x: x[0].lower()):
+ outfile.write(f'{theme}
{reason}
')
+ outfile.write(HTML_FOOTER.format(success=len(success), fail=len(fail)))
+
+ logger.info(f"built {len(success)} themes")
+ logger.info(f"failed {len(fail)} themes")
+
+
+def parse_args(argv=None):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--output", required=False, default="_output",
+ help="Output folder for generating the theme previews. Defaults to `_output` in themes folder root."
+ )
+ parser.add_argument(
+ "--samples", required=False, default="_pelican/samples",
+ help="Sample website used to generate theme previews. Defaults to `_pelican/samples` in themes folder root."
+ )
+ return parser.parse_args(argv)
+
+
+def check_requirements():
+ try:
+ proc = subprocess.run(
+ ["pelican", "--version"],
+ check=True, capture_output=True, universal_newlines=True
+ )
+ logger.info("using pelican: {}".format(proc.stdout.strip()))
+ except subprocess.CalledProcessError:
+ raise RuntimeError("Requires `pelican`, see https://docs.getpelican.com")
+ try:
+ proc = subprocess.run(
+ ["shot-scraper", "--version"],
+ check=True, capture_output=True, universal_newlines=True
+ )
+ logger.info("using shot-scraper: {}".format(proc.stdout.strip()))
+ except subprocess.CalledProcessError:
+ raise RuntimeError("Requires `shot-scraper`, see https://shot-scraper.data")
+
+
+def main(argv=None):
+ check_requirements()
+ args = parse_args(argv)
+ theme_root, samples_root, output_root, screenshot_root = setup_folders(args)
+ success, fail = build_theme_previews(theme_root, samples_root, output_root, screenshot_root)
+ write_index_files(output_root, success, fail)
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file