Skip to content

Commit 784308f

Browse files
authored
infrastructure framework for blog posts (#543)
* infrastructure framework * infrastructure working locally! * remove localize pacific * rm unformatted markdown from cards * fix link * space * space
1 parent 9cf898b commit 784308f

File tree

11 files changed

+279
-12
lines changed

11 files changed

+279
-12
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,7 @@ resource-gallery-submission-input.json
143143
.github/workflows/analytics-api/
144144
portal/metrics/*.png
145145
cisl-vast-pythia-*.json
146+
147+
# Blog post generation
148+
portal/atom.xml
149+
portal/rss.xml

.pre-commit-config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ repos:
77
- id: check-docstring-first
88
- id: check-json
99
- id: check-yaml
10-
- id: double-quote-string-fixer
1110

1211
- repo: https://github.com/psf/black
1312
rev: 25.1.0

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies:
1313
- numpy
1414
- matplotlib
1515
- google-api-python-client
16+
- feedgen
1617
- pip
1718
- pip:
1819
- google-analytics-data

portal/myst.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,34 @@ project:
66
id: 770e49e5-344a-4c46-adaa-3afb060b2085
77
authors: Project Pythia Community
88
github: https://github.com/projectpythia/projectpythia.github.io
9+
plugins:
10+
- type: executable
11+
path: src/blogpost.py
912

1013
toc:
1114
- file: index.md
1215
- file: about.md
13-
- title: Blog
16+
- file: posts/blog.md
1417
children:
15-
# - pattern: posts/*.md
16-
# Temporary until we have blog infrastructure: explicit list of posts by date (newest first)
18+
- title: "2025"
19+
children:
1720
- file: posts/2025/mystification.md
1821
- file: posts/2025/cookoff2025-website.md
1922
- file: posts/2025/binderhub_status.md
2023
- file: posts/2025/new-cookbooks.md
24+
- title: "2024"
25+
children:
2126
- file: posts/2024/cookoff2024-website.md
27+
- title: "2023"
28+
children:
2229
- file: posts/2023/cookoff2024-savethedate.md
2330
- file: posts/2023/fundraiser.md
2431
- file: posts/2023/cookoff2023.md
2532
- file: contributing.md
2633
- file: cookbook-guide.md
2734
- file: quick-cookbook-guide.md
2835
- file: metrics.md
36+
2937
site:
3038
domains: []
3139
options:

portal/posts/2023/cookoff2023.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ During the hackthon, significant additions were made to our Radar Cookbook and i
1616

1717
From our post-hackathon exit survey, everyone enjoyed the event, felt that they learned new skills and that their contributions were valued. One scientist commented, “The hackathon for me has become a great place to get a sense of a community. Seeing people enthusiastic about coding up notebooks that would benefit the research community is a gateway for someone starting to code in Python.” This comment mirrors the efforts of Project Pythia as a community-owned resource.
1818

19-
<img src="../_static/images/posts/projectpythia-cookbook-cookoff.jpeg" alt="Cookoff Image">
19+
<img src="../../_static/images/posts/projectpythia-cookbook-cookoff.jpeg" alt="Cookoff Image">

portal/posts/2025/mystification.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ We began the process of transitioning to MyST in the summer of 2024 at the annua
1414
The new MyST architecture was very appealing for several reasons:
1515
- **Sustainability**: Our current Sphinx-based architecture was becoming clunky and hard to maintain as members joined or left the project, and required too much boilerplate code in individual cookbook repos which presented a barrier to would-be new contributors. MyST offered a much more streamlined alternative to keep our community project growing.
1616
- **Staying on the leading edge of best practices**: We are an open-source community resource that teaches open-source coding practices, so it’s important that our own sites continue to be useful models for the broader community.
17-
- **Making cookbooks better!** A lot of the new functionality in MyST is really well suited to the cookbooks, including things like [cross-referencing](https://mystmd.org/guide/cross-references) and [embedding content](Embed & Include Content - MyST Markdown) and automated [bibliographies](https://mystmd.org/guide/citations).
17+
- **Making cookbooks better!** A lot of the new functionality in MyST is really well suited to the cookbooks, including things like [cross-referencing](https://mystmd.org/guide/cross-references) and [embedding content](https://mystmd.org/guide/embed) and automated [bibliographies](https://mystmd.org/guide/citations).
1818
- **Cross-pollination with the core developers!** Having the MyST developers invested in our use-case as a demo as they learn, understand, and develop functionality that will be particularly useful to us (and users that come after) is a really nice feedback loop from both a community and technological stand point.
1919

2020
## MyST for sustainability
2121
### Our aging infrastructure
22-
One example of our maintainability challenge was keeping our bespoke [Pythia-Sphinx theme](https://github.com/ProjectPythia/sphinx-pythia-theme) up-to-date. Upstream dependency updates and cascading syntax changes will always be a concern for the open source community. Combine that with browser default settingschanging since the birth of this project in 2020 (particularly for dark-mode), and there were many HTML and CSS customizations that were no longer displaying as intended. For this reason, we decided to stick as closely to the default [MyST book-theme](https://mystmd.org/guide/website-templates) as serves our purposes. The fewer moving pieces for a new contributor in our open source community to have to be spun up on the better. And with our current collaborations with the MyST team, it’s better to put effort into helping to improve the core tools rather than create unique new customizations.
22+
One example of our maintainability challenge was keeping our bespoke [Pythia-Sphinx theme](https://github.com/ProjectPythia/sphinx-pythia-theme) up-to-date. Upstream dependency updates and cascading syntax changes will always be a concern for the open source community. Combine that with browser default settings changing since the birth of this project in 2020 (particularly for dark-mode), and there were many HTML and CSS customizations that were no longer displaying as intended. For this reason, we decided to stick as closely to the default [MyST book-theme](https://mystmd.org/guide/website-templates) as serves our purposes. The fewer moving pieces for a new contributor in our open source community to have to be spun up on the better. And with our current collaborations with the MyST team, it’s better to put effort into helping to improve the core tools rather than create unique new customizations.
2323

2424
### Repository sprawl
2525
Another maintainability challenge was propagating changes across many GitHub repositories. Within the [Project Pythia Github organization](https://github.com/ProjectPythia/) we currently have 75 different repositories, the vast majority of which contain some website source under the big trenchcoat masquerading as one single Project Pythia website. Each repository is deployed within the domain, but there are separate repositories for our [home page](https://projectpythia.org/), [Foundations book](https://foundations.projectpythia.org/), [resource](https://projectpythia.org/resource-gallery/) and [Cookbooks galleries](https://cookbooks.projectpythia.org/), and for each individual Cookbook. With the Sphinx infrastructure, while the site theming could be abstracted into its own package, other changes to the site configuration or appearance, specifically of the links included in the top nav-bar or footer, would have to be individually updated in every single repository for consistency. We could update our Cookbook Template repository, but GitHub has no one way of sending those divergent-git-history changes to the various Cookbook repositories that leveraged that template. The MyST [`extends` keyword](https://mystmd.org/guide/frontmatter#composing-myst-yml) in the configuration file allows us to not only abstract theming, but also configuration commands and content. Future changes to the site navbar will only have to be made in one place, and individual Cookbook authors will be able to focus on their own content with much reduced boilerplate!

portal/posts/2025/new-cookbooks.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,11 @@ This Cookbook covers how to work with wavelets in Python. Wavelets are a powerfu
173173

174174
### In pictures
175175

176-
<img src="../_static/images/posts/IMG_5686.jpeg" alt="2024 Cookoff group photo" width=900>
176+
<img src="../../_static/images/posts/IMG_5686.jpeg" alt="2024 Cookoff group photo" width=900>
177177
<br>
178-
<img src="../_static/images/posts/IMG_5854.jpeg" alt="Breakout group" width=250>
179-
<img src="../_static/images/posts/IMG_6546.jpg" alt="Cookoff attendees at Mesa Lab" width=250>
180-
<img src="../_static/images/posts/IMG_6055.jpeg" alt="Virtual presentation" width=250>
178+
<img src="../../_static/images/posts/IMG_5854.jpeg" alt="Breakout group" width=250>
179+
<img src="../../_static/images/posts/IMG_6546.jpg" alt="Cookoff attendees at Mesa Lab" width=250>
180+
<img src="../../_static/images/posts/IMG_6055.jpeg" alt="Virtual presentation" width=250>
181181

182182
### By the numbers
183183

portal/posts/blog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Blog
2+
3+
Below is the latest news from Project Pythia.
4+
5+
:::{postlist}
6+
:number: 25
7+
:::

portal/src/blogpost.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import json
4+
import re
5+
import sys
6+
from pathlib import Path
7+
8+
import pandas as pd
9+
import unist as u
10+
from feedgen.feed import FeedGenerator
11+
from yaml import safe_load
12+
13+
DEFAULTS = {"number": 10}
14+
15+
root = Path(__file__).parent.parent
16+
17+
# Aggregate all posts from the markdown and ipynb files
18+
posts = []
19+
for ifile in root.rglob("posts/**/*.md"):
20+
if "drafts" in str(ifile):
21+
continue
22+
23+
text = ifile.read_text()
24+
try:
25+
_, meta, content = text.split("---", 2)
26+
except Exception:
27+
print(f"Skipping file with error: {ifile}", file=sys.stderr)
28+
continue
29+
30+
# Load in YAML metadata
31+
meta = safe_load(meta)
32+
meta["path"] = ifile.relative_to(root).with_suffix("")
33+
if "title" not in meta:
34+
lines = text.splitlines()
35+
for ii in lines:
36+
if ii.strip().startswith("#"):
37+
meta["title"] = ii.replace("#", "").strip()
38+
break
39+
40+
# Summarize content
41+
skip_lines = ["#", "--", "%", "++"]
42+
content = "\n".join(
43+
ii
44+
for ii in content.splitlines()
45+
if not any(ii.startswith(char) for char in skip_lines)
46+
)
47+
48+
N_WORDS = 50
49+
content_no_links = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", content)
50+
content_no_bold = re.sub(r"\*\*", "", content_no_links)
51+
words = " ".join(content_no_bold.split(" ")[:N_WORDS])
52+
53+
if "author" not in meta:
54+
meta["author"] = "Project Pythia Team"
55+
meta["content"] = meta.get("description", words)
56+
posts.append(meta)
57+
posts = pd.DataFrame(posts)
58+
posts["date"] = pd.to_datetime(posts["date"]).dt.tz_localize("UTC")
59+
posts = posts.dropna(subset=["date"])
60+
posts = posts.sort_values("date", ascending=False)
61+
62+
# Generate an RSS feed
63+
fg = FeedGenerator()
64+
fg.id("https://projectpythia.org/")
65+
fg.title("Project Pythia blog")
66+
fg.author({"name": "Project Pytia Team", "email": "[email protected]"})
67+
fg.link(href="https://projectpythia.org/", rel="alternate")
68+
fg.logo("https://projectpythia.org/_static/profile.jpg")
69+
fg.subtitle("Project Pythia blog!")
70+
fg.link(href="https://projectpythia.org/", rel="self")
71+
fg.language("en")
72+
73+
# Add all my posts to it
74+
for ix, irow in posts.iterrows():
75+
fe = fg.add_entry()
76+
fe.id(f"https://projectpythia.org/{irow['path']}")
77+
fe.published(irow["date"])
78+
fe.title(irow["title"])
79+
fe.link(href=f"https://projectpythia.org/{irow['path']}")
80+
fe.content(content=irow["content"])
81+
82+
# Write an RSS feed with latest posts
83+
fg.atom_file(root / "atom.xml", pretty=True)
84+
fg.rss_file(root / "rss.xml", pretty=True)
85+
86+
plugin = {
87+
"name": "Blog Post list",
88+
"directives": [
89+
{
90+
"name": "postlist",
91+
"doc": "An example directive for showing a nice random image at a custom size.",
92+
"alias": ["bloglist"],
93+
"arg": {},
94+
"options": {
95+
"number": {
96+
"type": "int",
97+
"doc": "The number of posts to include",
98+
}
99+
},
100+
}
101+
],
102+
}
103+
104+
children = []
105+
for ix, irow in posts.iterrows():
106+
children.append(
107+
{
108+
"type": "card",
109+
"url": f"/{irow['path'].with_suffix('')}",
110+
"children": [
111+
{"type": "cardTitle", "children": [u.text(irow["title"])]},
112+
{"type": "paragraph", "children": [u.text(irow["content"])]},
113+
{
114+
"type": "footer",
115+
"children": [
116+
u.strong([u.text("Date: ")]),
117+
u.text(f"{irow['date']:%B %d, %Y} | "),
118+
u.strong([u.text("Author: ")]),
119+
u.text(f"{irow['author']}"),
120+
],
121+
},
122+
],
123+
}
124+
)
125+
126+
127+
def declare_result(content):
128+
"""Declare result as JSON to stdout
129+
130+
:param content: content to declare as the result
131+
"""
132+
133+
# Format result and write to stdout
134+
json.dump(content, sys.stdout, indent=2)
135+
# Successfully exit
136+
raise SystemExit(0)
137+
138+
139+
def run_directive(name, data):
140+
"""Execute a directive with the given name and data
141+
142+
:param name: name of the directive to run
143+
:param data: data of the directive to run
144+
"""
145+
assert name == "postlist"
146+
opts = data["node"].get("options", {})
147+
number = int(opts.get("number", DEFAULTS["number"]))
148+
output = children[:number]
149+
return output
150+
151+
152+
if __name__ == "__main__":
153+
parser = argparse.ArgumentParser()
154+
group = parser.add_mutually_exclusive_group()
155+
group.add_argument("--role")
156+
group.add_argument("--directive")
157+
group.add_argument("--transform")
158+
args = parser.parse_args()
159+
160+
if args.directive:
161+
data = json.load(sys.stdin)
162+
declare_result(run_directive(args.directive, data))
163+
elif args.transform:
164+
raise NotImplementedError
165+
elif args.role:
166+
raise NotImplementedError
167+
else:
168+
declare_result(plugin)

portal/src/unist.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Copied from:
3+
https://github.com/projectpythia-mystmd/cookbook-gallery/blob/main/unist.py
4+
"""
5+
6+
7+
# Node Creation Tools
8+
def text(value, **opts):
9+
return {"type": "text", "value": value, **opts}
10+
11+
12+
def strong(children, **opts):
13+
return {"type": "strong", "children": children, **opts}
14+
15+
16+
def link(children, url, **opts):
17+
return {"type": "link", "url": url, "children": children, **opts}
18+
19+
20+
def table(children, **opts):
21+
return {"type": "table", "children": children, **opts}
22+
23+
24+
def table_cell(children, **opts):
25+
return {"type": "tableCell", "children": children, **opts}
26+
27+
28+
def table_row(cells, **opts):
29+
return {"type": "tableRow", "children": cells, **opts}
30+
31+
32+
def span(children, style, **opts):
33+
return {"type": "span", "children": children, "style": style, **opts}
34+
35+
36+
def definition_list(children, **opts):
37+
return {"type": "definitionList", "children": children, **opts}
38+
39+
40+
def definition_term(children, **opts):
41+
return {"type": "definitionTerm", "children": children, **opts}
42+
43+
44+
def definition_description(children, **opts):
45+
return {"type": "definitionDescription", "children": children, **opts}
46+
47+
48+
def list_(children, ordered=False, spread=False, **opts):
49+
return {
50+
"type": "list",
51+
"ordered": ordered,
52+
"spread": spread,
53+
"children": children,
54+
**opts,
55+
}
56+
57+
58+
def list_item(children, spread=True, **opts):
59+
return {"type": "listItem", "spread": spread, "children": children, **opts}
60+
61+
62+
def image(url, **opts):
63+
return {"type": "image", "url": url, **opts}
64+
65+
66+
def grid(columns, children, **opts):
67+
return {"type": "grid", "columns": columns, "children": children, **opts}
68+
69+
70+
def div(children, **opts):
71+
return {"type": "div", "children": children, **opts}
72+
73+
74+
def find_all_by_type(parent: dict, type_: str):
75+
for node in parent["children"]:
76+
if node["type"] == type_:
77+
yield node
78+
if "children" not in node:
79+
continue
80+
yield from find_all_by_type(node, type_)

0 commit comments

Comments
 (0)