Reorganize http resources and build logic

This commit is contained in:
Ethan Paul 2022-01-31 23:01:07 -05:00
parent 691f92d90d
commit 5ab9ad02de
No known key found for this signature in database
GPG Key ID: D0E2CBF1245E92BF
17 changed files with 849 additions and 503 deletions

481
build.py
View File

@ -1,14 +1,18 @@
import argparse
import datetime
import hashlib
import shutil
import sys
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
import jinja2
@ -19,80 +23,108 @@ import ruamel.yaml
yaml = ruamel.yaml.YAML(typ="safe")
@dataclass
class MediaContainer:
title: str
source: str
preview: Optional[str] = None
anchor: Optional[Union[str, int]] = None
content: Optional[str] = None
hide_source: bool = False
def multi_replace(source: str, replacements: Sequence[Tuple[str, str]]) -> str:
for old, new in replacements:
replaced = source.replace(old, new)
return replaced
class MediaSerializer(msh.Schema):
title = msh.fields.String(required=True)
source = msh.fields.String(required=True)
preview = msh.fields.String(required=False)
anchor = msh.fields.String(required=False)
content = msh.fields.String(required=False)
hide_source = msh.fields.Boolean(required=False)
class PathField(msh.fields.String):
def _deserialize(self, value, *args, **kwargs):
return Path(value).expanduser().resolve()
class BaseSchema(msh.Schema):
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs):
return self.Container(**data)
class MediaSerializer(BaseSchema):
@dataclass
class Container:
title: str
link: str
anchor: str
content: Optional[str]
def preload_url(self, config) -> str:
if config.build.kodak:
return f"{config.build.kodak.baseurl}image/{self.link}/{config.build.kodak.preload}.jpeg"
return self.link
def asset_url(self, config) -> str:
if config.build.kodak:
return f"{config.build.kodak.baseurl}image/{self.link}/{config.build.kodak.asset}.jpeg"
return self.link
def source_url(self, config) -> str:
if config.build.kodak:
return f"{config.build.kodak.baseurl}image/{self.link}/original"
return self.link
title = msh.fields.String()
link = msh.fields.String()
anchor = msh.fields.String(allow_none=True, missing=None)
content = msh.fields.String(allow_none=True, missing=None)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> MediaContainer:
return MediaContainer(**data)
def _make_default_anchor(self, data, **kwargs):
if not data.anchor:
data.anchor = multi_replace(
data.title, [(" ", "-"), ("?", ""), ("!", ""), (".", ""), (":", "")]
)
return data
@dataclass
class LinkContainer:
link: str
title: Optional[str] = None
icon: Optional[str] = None
class LinkSerializer(BaseSchema):
@dataclass
class Container:
title: Optional[str]
url: str
icon: str
url = msh.fields.URL()
title = msh.fields.String(allow_none=True, missing=None)
icon = msh.fields.String(missing="fas fa-external-link-square-alt")
class LinkSerializer(msh.Schema):
link = msh.fields.URL(required=True)
title = msh.fields.String(required=False)
icon = msh.fields.String(required=False)
class LocationSeralizer(BaseSchema):
class Container(NamedTuple):
title: str
link: str
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> LinkContainer:
return LinkContainer(**data)
title = msh.fields.String()
link = msh.fields.URL()
class Location(NamedTuple):
title: str
link: str
class PostSerializer(BaseSchema):
@dataclass
class Container:
title: str
description: Optional[str]
location: LocationSeralizer.Container
date: datetime.date
banner: Optional[str]
slug: str
links: Sequence[LinkSerializer.Container]
media: Sequence[MediaSerializer.Container]
def banner_url(self, config) -> str:
if config.build.kodak:
return f"{config.build.kodak.baseurl}image/{self.banner}/{config.build.kodak.banner}.jpeg"
return self.banner
class LocationSeralizer(msh.Schema):
title = msh.fields.String(required=True)
link = msh.fields.URL(required=True)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> Location:
return Location(**data)
@dataclass
class PostContainer:
title: str
location: Location
date: datetime.date
banner: str
media: Sequence[MediaContainer]
links: Sequence[LinkContainer] = ()
slug: Optional[str] = None
class PostSerializer(msh.Schema):
title = msh.fields.String(required=True)
location = msh.fields.Nested(LocationSeralizer, required=True)
date = msh.fields.Date("%Y-%m-%d", required=True)
banner = msh.fields.URL(required=True)
slug = msh.fields.String(required=False)
links = msh.fields.List(msh.fields.Nested(LinkSerializer), required=False)
media = msh.fields.List(msh.fields.Nested(MediaSerializer), required=True)
title = msh.fields.String()
description = msh.fields.String(missing=None, allow_none=True)
location = msh.fields.Nested(LocationSeralizer)
date = msh.fields.Raw()
banner = msh.fields.String(missing=None, allow_none=True)
slug = msh.fields.String(
validate=msh.validate.Regexp(r"^[a-z0-9][a-z0-9\-]+[a-z0-9]$")
)
links = msh.fields.List(msh.fields.Nested(LinkSerializer), missing=list())
media = msh.fields.List(msh.fields.Nested(MediaSerializer), missing=list())
@msh.validates_schema
def _unique_anchors(self, data: Dict[str, Any], **kwargs):
@ -102,87 +134,306 @@ class PostSerializer(msh.Schema):
f"Media anchors used multiple times: {set([item for item in anchors if anchors.count(item) > 1])}"
)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> PostContainer:
for index, item in enumerate(data["media"]):
item.anchor = item.anchor or index
data["media"][index] = item
return PostContainer(**data)
class ConfigBuildKodakSerializer(BaseSchema):
@dataclass
class Container:
baseurl: str
link_original: bool
asset: str
banner: str
preload: str
baseurl = msh.fields.URL()
link_original = msh.fields.Boolean(missing=False)
asset = msh.fields.String()
banner = msh.fields.String()
preload = msh.fields.String()
class ConfigSerializer(msh.Schema):
static = msh.fields.List(msh.fields.String(), required=False)
posts = msh.fields.List(msh.fields.Nested(PostSerializer), required=True)
class ConfigBuildSerializer(BaseSchema):
@dataclass
class Container:
generated: Path
posts: Path
static: Path
bundle: Path
templates: Path
post_base: str
kodak: ConfigBuildKodakSerializer.Container
@msh.validates_schema
def _unique_slugs(self, data: Dict[str, Any], **kwargs):
slugs = [item.slug for item in data["posts"] if item.slug is not None]
if len(slugs) != len(set(slugs)):
raise msh.ValidationError(
f"Post slugs used multiple times: {set([item for item in slugs if slugs.count(item) > 1])}"
generated = PathField(missing=Path("publish"))
posts = PathField(missing=Path("posts"))
static = PathField(missing=Path("static"))
bundle = PathField(missing=Path("bundle"))
templates = PathField(missing=Path("templates"))
post_base = msh.fields.String(
missing="explore", validate=msh.validate.Regexp(r"[a-z0-9\-]+")
)
kodak = msh.fields.Nested(ConfigBuildKodakSerializer, missing=None)
class ConfigSerializer(BaseSchema):
@dataclass
class Container:
domain: str
https: bool
baseurl: str
title: str
email: str
description: str
keywords: Sequence[str]
social: Dict[str, str]
build: ConfigBuildSerializer.Container
@property
def url(self) -> str:
return f"http{'s' if self.https else ''}://{self.domain}{self.baseurl}"
domain = msh.fields.String()
https = msh.fields.Boolean(missing=True)
baseurl = msh.fields.String()
title = msh.fields.String()
email = msh.fields.Email()
description = msh.fields.String()
keywords = msh.fields.List(
msh.fields.String(validate=msh.validate.Regexp(r"^[a-z0-9]+$"))
)
social = msh.fields.Dict(
keys=msh.fields.String(
validate=msh.validate.OneOf(
["instagram", "facebook", "twitter", "mastodon", "patreon"]
)
@msh.post_load
def _remove_future(self, data: Dict[str, Any], *args, **kwargs) -> PostContainer:
posts = [item for item in data["posts"] if item.date <= datetime.date.today()]
data["posts"] = posts
return data
),
values=msh.fields.Url(),
missing=dict(),
)
build = msh.fields.Nested(ConfigBuildSerializer)
def get_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--config", help="Path to the config file", default=(Path.cwd() / "config.yaml")
"-c",
"--config",
help="Path to the config file",
default=(Path.cwd() / "config.yaml"),
)
parser.add_argument(
"-c", "--check", action="store_true", help="Check the config without building"
"--check", action="store_true", help="Check the config without building"
)
parser.add_argument(
"--dev",
action="store_true",
help="Run local development server",
)
return parser.parse_args()
def _hash_from_file(path: Union[str, Path]):
"""Construct from a file path, generating the hash of the file
.. note:: This method attempts to _efficiently_ compute a hash of large image files. The
hashing code was adapted from here:
https://stackoverflow.com/a/44873382/5361209
"""
hasher = hashlib.sha256()
view = memoryview(bytearray(1024 * 1024))
with Path(path).open("rb", buffering=0) as infile:
for chunk in iter(lambda: infile.readinto(view), 0): # type: ignore
hasher.update(view[:chunk])
return hasher
def _copy_resource(path: Path, dest_dir: Path):
if path.is_file():
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path, dest_dir / path.name, follow_symlinks=True)
elif path.is_dir():
for item in path.iterdir():
_copy_resource(item, dest_dir / path.name)
def _write_template(env: jinja2.Environment, name: str, dest: Path, **kwargs):
dest.parent.mkdir(exist_ok=True)
template = env.get_template(name).render(**kwargs)
with dest.open("w") as outfile:
outfile.write(template)
def _build_bundle(
config: ConfigSerializer.Container, ftype: str, dest: str, sources: List[str]
) -> str:
(config.build.generated / ftype.lower()).mkdir(exist_ok=True, parents=True)
working_path = (
config.build.generated / ftype.lower() / f"{uuid.uuid4().hex}.{ftype.lower()}"
)
with working_path.open("w") as outfile:
for source in sources:
try:
with (
config.build.bundle / ftype.lower() / f"{source}.{ftype.lower()}"
).open("r") as infile:
outfile.write(infile.read())
outfile.write("\n\n")
except FileNotFoundError as err:
raise ValueError(
f"No {ftype.upper()} source file to bundle named '{source}'"
) from err
bundle_hash = _hash_from_file(working_path)
slug = f"{dest}-{bundle_hash.hexdigest()[:8]}"
final_path = config.build.generated / ftype.lower() / f"{slug}.{ftype.lower()}"
working_path.rename(final_path)
return slug
def _dev(
cwd: Path,
config: ConfigSerializer.Container,
posts: Sequence[PostSerializer.Container],
):
config.https = False
config.domain = "localhost:5000"
config.base_url = "/"
# server = http.server.HTTPServer(
# ("127.0.0.1", 5000),
# functools.partial(
# http.server.SimpleHTTPRequestHandler, directory=str(cwd / config.build.generated)
# ),
# )
_build(cwd, config, posts)
# print(f"Serving dev site at {config.url}, press Ctrl+C to exit", file=sys.stderr)
# try:
# server.serve_forever()
# except KeyboardInterrupt:
# print("Stopping...", file=sys.stderr)
# server.shutdown()
def _build(
cwd: Path,
config: ConfigSerializer.Container,
posts: Sequence[PostSerializer.Container],
):
print(
f"Rebuilding static assets into {cwd / config.build.generated}", file=sys.stderr
)
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(str(cwd / config.build.templates)),
autoescape=jinja2.select_autoescape(["html", "xml"]),
)
output = cwd / config.build.generated
static = cwd / config.build.static
today = datetime.datetime.utcnow()
bundle_slug = uuid.uuid4().hex[:8]
index_css_bundle = _build_bundle(config, "css", "index", ["common", "home"])
index_js_bundle = _build_bundle(
config, "js", "index", ["random-background", "preloader"]
)
_write_template(
env,
"index.html.j2",
output / "index.html",
config=config,
today=today,
css_bundle=index_css_bundle,
js_bundle=index_js_bundle,
)
_write_template(
env, "sitemap.xml.j2", output / "sitemap.xml", config=config, today=today
)
_write_template(
env,
"robots.txt.j2",
output / "robots.txt",
config=config,
today=today,
disallowed=[item.name for item in static.iterdir() if item.is_dir()],
)
static = cwd / config.build.static
if static.exists():
for item in static.iterdir():
_copy_resource(item, output)
explore_css_bundle = _build_bundle(config, "css", "explore", ["common", "explore"])
explore_js_bundle = _build_bundle(
config,
"js",
"explore",
["random-background", "preloader", "toggle-article-text-button"],
)
_write_template(
env,
"explore.html.j2",
output / config.build.post_base / "index.html",
config=config,
today=today,
posts=posts,
css_bundle=explore_css_bundle,
js_bundle=explore_js_bundle,
)
post_css_bundle = _build_bundle(config, "css", "post", ["common"])
post_js_bundle = _build_bundle(config, "js", "post", ["preloader"])
for post in posts:
_write_template(
env,
"post.html.j2",
output / config.build.post_base / post.slug / "index.html",
config=config,
today=today,
post=post,
css_bundle=post_css_bundle,
js_bundle=post_js_bundle,
)
def main():
args = get_args()
cwd = Path.cwd().resolve()
output = cwd / "export"
explore = output / "explore"
with Path(args.config).resolve().open() as infile:
with Path(args.config).resolve().open(encoding="utf-8") as infile:
config = ConfigSerializer().load(yaml.load(infile))
posts = []
post_serializer = PostSerializer()
for item in (cwd / config.build.posts).iterdir():
if item.suffix.lower() == ".yaml":
with item.open() as infile:
raw = yaml.load(infile)
raw["slug"] = raw.get("slug", item.stem)
posts.append(post_serializer.load(raw))
slugs = [post.slug for post in posts]
if len(set(slugs)) != len(slugs):
raise msh.ValidationError("Duplicate post slugs found in config")
if args.check:
print("Config check successful!", file=sys.stderr)
return 0
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(str(cwd / "templates")),
autoescape=jinja2.select_autoescape(["html", "xml"]),
)
posts = sorted(posts, key=lambda item: item.date, reverse=True)
output.mkdir(exist_ok=True)
explore.mkdir(exist_ok=True)
if args.dev:
_dev(cwd, config, posts)
else:
_build(cwd, config, posts)
index = env.get_template("index.html.j2").render(config=config)
with (explore / "index.html").open("w") as outfile:
outfile.write(index)
sitemap = env.get_template("sitemap.xml.j2").render(config=config)
with (output / "sitemap.xml").open("w") as outfile:
outfile.write(sitemap)
for static in config["static"]:
dest = Path(output / static).resolve()
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(static, str(output / static), follow_symlinks=True)
post_template = env.get_template("post.html.j2")
for post_data in config["posts"]:
post = post_template.render(post=post_data)
with (explore / f"{post_data.slug}.html").open("w") as outfile:
outfile.write(post)
nginx = env.get_template("nginx.conf.d.j2").render(config=config)
with (cwd / "nginx.conf").open("w") as outfile:
outfile.write(nginx)
return 0
if __name__ == "__main__":

View File

@ -5,19 +5,13 @@ html {
font-family: Verdana, Helvetica, sans-serif;
}
a {
color: inherit;
text-decoration: none;
transition: all 0.1s ease-in-out;
}
a:hover {
text-decoration: none;
text-shadow: 5px 5px 10px #fff, -5px -5px 10px #fff;
body {
color: white;
font-family: sans-serif;
}
#background-image {
background-image: url("https://cdn.enp.one/img/backgrounds/cl-photo-allis.jpg");
background-image: url("https://cdn.enp.one/img/backgrounds/cl-photo-rt112.jpg");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
@ -38,136 +32,13 @@ a:hover {
z-index: 0;
}
#content {
text-align: center;
text-shadow: 3px 3px 5px #000, -3px -3px 5px #000;
font-weight: bold;
color: white;
padding: 1em;
width: 40em;
max-width: 90%;
background-color: rgba(0, 0, 0, 0.4);
border-style: solid;
border-width: 2px;
border-color: rgba(0, 0, 0, 0);
border-radius: 128px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.6), 0 6px 20px 0 rgba(0, 0, 0, 0.6);
position: absolute;
top: 15%;
left: 50%;
transform: translate(-50%, 0);
z-index: 10;
}
#logo {
margin: auto;
margin-top: -5em;
max-width: 60%;
width: 50%;
display: block;
border-style: solid;
border-color: rgba(0, 0, 0, 0.2);
border-radius: 50%;
border-width: 5px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4), 0 6px 20px 0 rgba(0, 0, 0, 0.4);
}
h1 {
font-variant: small-caps;
font-size: 2.5em;
}
#content p {
margin: 2em;
line-height: 1.5;
}
ul.buttons {
list-style: none;
padding-left: 0;
margin-top: 1em;
margin-bottom: 1em;
font-size: 1.75em;
}
ul.buttons li {
line-height: 1;
padding: 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
text-transform: uppercase;
}
.button.nav {
padding-top: 0.75em;
padding-bottom: 0.55em;
padding-left: 1.5em;
padding-right: 1.5em;
border-radius: 30px;
transition: all 0.25s ease-in-out;
}
.button:hover {
text-shadow:
-3px -3px 5px #fff,
-3px 3px 5px #fff,
3px -3px 5px #fff,
3px 3px 5px #fff,
0px 0px 7px #ff0000;
}
.button.nav:hover {
text-shadow:
0px 0px 7px #000,
-5px -5px 10px #fff,
-5px 5px 10px #fff,
5px -5px 10px #fff,
5px 5px 10px #fff;
-webkit-animation-name: pulse;
-webkit-animation-duration: 5s;
-webkit-animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-webkit-animation-fill-mode: none;
animation-name: pulse;
animation-duration: 5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-fill-mode: none;
}
.explore:hover { color: #5588e0; }
.youtube:hover { color: #ff0000; }
.instagram:hover { color: #c13584; }
.twitter:hover { color: #1da1f2; }
#background-info {
text-align: right;
font-size: 0.85em;
padding: 0.75em;
#background-image .overlay {
background-color: rgba(0, 0, 0, 0.8);
width: 100%;
height: 100%;
position: fixed;
bottom: 0;
right: 0;
z-index: 5;
}
footer { font-size: 0.9em; }
footer div { margin-bottom: 0.5em; }
footer a.button i {
padding: 0.5em;
font-size: 1.25em;
top: 0;
left: 0;
}
.fadeout {
@ -227,49 +98,6 @@ footer a.button i {
100% {opacity: 0;}
}
@keyframes pulse {
0% {
box-shadow:
0px 0px 15px 3px #fff,
0px 0px 15px 3px #88a9fc;
}
10% {
box-shadow:
-10px -10px 15px 3px #fff,
10px 10px 15px 3px #88a9fc;
}
30% {
box-shadow:
-10px 10px 15px 3px #b5f7fc,
10px -10px 15px 3px #fcaa99;
}
45% {
box-shadow:
10px 10px 15px 3px #ecf9a7,
-10px -10px 15px 3px #fcaa99;
}
60% {
box-shadow:
10px -10px 15px 3px #ecf9a7,
-10px 10px 15px 3px #abfcad;
}
75% {
box-shadow:
-10px -10px 15px 3px #b5f7fc,
10px 10px 15px 3px #abfcad;
}
90% {
box-shadow:
-10px 10px 15px 3px #fff,
10px -10px 15px 3px #88a9fc;
}
100% {
box-shadow:
0px 0px 15px 3px #b5f7fc,
0px 0px 15px 3px #88a9fc;
}
}
@-webkit-keyframes spinner {
0% { transform: translate(-50%,-50%) rotate(0deg); }
100% { transform: translate(-50%,-50%) rotate(360deg); }
@ -280,25 +108,67 @@ footer a.button i {
100% { transform: translate(-50%,-50%) rotate(360deg); }
}
@media only screen and (max-width: 600px) {
#content {
padding: 0;
padding-bottom: 1em;
border-radius: 32px;
top: 6em;
}
#content p {
margin: 1em;
}
ul.buttons {
margin-top: 1.5em;
margin-bottom: 1.5em;
}
ul.buttons li {
display: block;
margin-top: 1em;
}
a {
color: inherit;
text-decoration: none;
transition: all 0.1s ease-in-out;
}
a:hover {
text-decoration: none;
text-shadow: 5px 5px 10px #fff, -5px -5px 10px #fff;
}
ul.buttons {
list-style: none;
padding-left: 0;
margin-top: 1em;
margin-bottom: 1em;
font-size: 1.75em;
}
ul.buttons li {
line-height: 1;
padding: 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
text-transform: uppercase;
}
.button:hover {
text-shadow:
-3px -3px 5px #fff,
-3px 3px 5px #fff,
3px -3px 5px #fff,
3px 3px 5px #fff,
0px 0px 7px #ff0000;
}
.button.nav:hover {
text-shadow:
0px 0px 7px #000,
-5px -5px 10px #fff,
-5px 5px 10px #fff,
5px -5px 10px #fff,
5px 5px 10px #fff;
-webkit-animation-name: pulse;
-webkit-animation-duration: 5s;
-webkit-animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-webkit-animation-fill-mode: none;
animation-name: pulse;
animation-duration: 5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-fill-mode: none;
}
.explore:hover { color: #5588e0; }
.youtube:hover { color: #ff0000; }
.instagram:hover { color: #c13584; }
.twitter:hover { color: #1da1f2; }

View File

@ -1,53 +1,8 @@
html {
background-color: black;
}
body {
color: white;
font-family: sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
ul {
list-style: none;
padding: 0;
}
#background-image {
background-image: url("https://cdn.enp.one/img/backgrounds/cl-photo-rt112.jpg");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
filter: blur(6px);
-webkit-filter: blur(6px);
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
#background-image .overlay {
background-color: rgba(0, 0, 0, 0.8);
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
}
#toggle-description {
position: fixed;
right: 0;
@ -74,17 +29,26 @@ ul {
color: black;
}
#header h1 {
#header {
font-variant: small-caps;
border-bottom-style: solid;
margin-left: auto;
margin-right: auto;
margin-bottom: 2em;
margin-top: 1em;
padding-bottom: 1em;
width: 75%;
text-shadow: 3px 3px 5px #000;
text-align: left;
margin-bottom: 2em;
margin-top: 1em;
}
#header h1 {
border-bottom-style: solid;
padding-bottom: 1em;
margin-left: auto;
margin-right: auto;
width: 75%;
}
#header p {
margin-left: auto;
margin-right: auto;
width: 75%;
}
#header span {
@ -170,3 +134,27 @@ ul {
margin-left: 1em;
margin-right: 0.7em;
}
@media only screen and (max-width: 600px) {
h1 { font-size: 1.5rem; }
h2 { font-size: 1.25rem; }
p { font-size: 0.9rem; }
#toggle-description { font-size: 1.25rem; }
.article {
border-radius: 3em;
margin-bottom: 1em;
}
.article-banner {
border-radius: 3em;
}
.article-content {
padding-left: 2em;
padding-right: 2em;
}
}

143
bundle/css/home.css Normal file
View File

@ -0,0 +1,143 @@
#content {
text-align: center;
text-shadow: 3px 3px 5px #000, -3px -3px 5px #000;
font-weight: bold;
color: white;
padding: 1em;
width: 40em;
max-width: 90%;
background-color: rgba(0, 0, 0, 0.4);
border-style: solid;
border-width: 2px;
border-color: rgba(0, 0, 0, 0);
border-radius: 128px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.6), 0 6px 20px 0 rgba(0, 0, 0, 0.6);
position: absolute;
top: 15%;
left: 50%;
transform: translate(-50%, 0);
z-index: 10;
}
#logo {
margin: auto;
margin-top: -5em;
max-width: 60%;
width: 50%;
display: block;
border-style: solid;
border-color: rgba(0, 0, 0, 0.2);
border-radius: 50%;
border-width: 5px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4), 0 6px 20px 0 rgba(0, 0, 0, 0.4);
}
h1 {
font-variant: small-caps;
font-size: 2.5em;
}
#content p {
margin: 2em;
line-height: 1.5;
}
.button.nav {
padding-top: 0.75em;
padding-bottom: 0.55em;
padding-left: 1.5em;
padding-right: 1.5em;
border-radius: 30px;
transition: all 0.25s ease-in-out;
}
#background-info {
text-align: right;
font-size: 0.85em;
padding: 0.75em;
position: fixed;
bottom: 0;
right: 0;
z-index: 5;
}
footer { font-size: 0.9em; }
footer div { margin-bottom: 0.5em; }
footer a.button i {
padding: 0.5em;
font-size: 1.25em;
}
@keyframes pulse {
0% {
box-shadow:
0px 0px 15px 3px #fff,
0px 0px 15px 3px #88a9fc;
}
10% {
box-shadow:
-10px -10px 15px 3px #fff,
10px 10px 15px 3px #88a9fc;
}
30% {
box-shadow:
-10px 10px 15px 3px #b5f7fc,
10px -10px 15px 3px #fcaa99;
}
45% {
box-shadow:
10px 10px 15px 3px #ecf9a7,
-10px -10px 15px 3px #fcaa99;
}
60% {
box-shadow:
10px -10px 15px 3px #ecf9a7,
-10px 10px 15px 3px #abfcad;
}
75% {
box-shadow:
-10px -10px 15px 3px #b5f7fc,
10px 10px 15px 3px #abfcad;
}
90% {
box-shadow:
-10px 10px 15px 3px #fff,
10px -10px 15px 3px #88a9fc;
}
100% {
box-shadow:
0px 0px 15px 3px #b5f7fc,
0px 0px 15px 3px #88a9fc;
}
}
@media only screen and (max-width: 600px) {
#content {
padding: 0;
padding-bottom: 1em;
border-radius: 32px;
top: 6em;
}
#content p {
margin: 1em;
}
ul.buttons {
margin-top: 1.5em;
margin-bottom: 1.5em;
}
ul.buttons li {
display: block;
margin-top: 1em;
}
}

8
bundle/js/preloader.js Normal file
View File

@ -0,0 +1,8 @@
window.addEventListener("load", async function() {
document.getElementById("preloader").classList.add("fadeout");
// I don't actually know how promises or async works
// ¯\_(ツ)_/¯
// https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep
await new Promise(r => setTimeout(r, 250))
document.getElementById("preloader").style.display = "none";
});

View File

@ -66,7 +66,6 @@ const BACKGROUND_IMAGES = [
}
];
function selectBackground() {
let max = BACKGROUND_IMAGES.length - 1
let min = 0;
@ -75,42 +74,10 @@ function selectBackground() {
return BACKGROUND_IMAGES[index];
}
function togglePrimaryText() {
let items = document.getElementsByClassName("article");
for (index = 0; index < items.length; index++) {
if (items[index].classList.contains("primary-text")) {
items[index].classList.remove("primary-text");
} else {
items[index].classList.add("primary-text");
}
}
let button = document.getElementById("toggle-description");
if (button.classList.contains("active")) {
button.classList.remove("active");
} else {
button.classList.add("active");
}
};
window.addEventListener("DOMContentLoaded", function () {
window.addEventListener("DOMContentLoaded", function() {
let selected = selectBackground()
document.getElementById(
"background-image"
).style.backgroundImage = "url(" + selected.url + ")";
});
window.addEventListener("load", async function () {
document.getElementById("toggle-description").addEventListener("click", togglePrimaryText);
document.getElementById("preloader").classList.add("fadeout");
// I don't actually know how promises or async works
// ¯\_(ツ)_/¯
// https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep
await new Promise(r => setTimeout(r, 250))
document.getElementById("preloader").style.display = "none";
});

View File

@ -0,0 +1,23 @@
function togglePrimaryText() {
let items = document.getElementsByClassName("article");
for (index = 0; index < items.length; index++) {
if (items[index].classList.contains("primary-text")) {
items[index].classList.remove("primary-text");
} else {
items[index].classList.add("primary-text");
}
}
let button = document.getElementById("toggle-description");
if (button.classList.contains("active")) {
button.classList.remove("active");
} else {
button.classList.add("active");
}
};
window.addEventListener("load", async function() {
document.getElementById("toggle-description").addEventListener("click", togglePrimaryText);
});

40
templates/explore.html.j2 Normal file
View File

@ -0,0 +1,40 @@
{% from "macros.html.j2" import make_header %}{% from "macros.html.j2" import make_social_links %}<!DOCTYPE html>
<html lang="en">
{{ make_header(config, alttitle="Explore " + config.title, css_bundle=css_bundle, js_bundle=js_bundle) }}
<body>
<div id="background-image"><div class="overlay"></div></div>
<div id="preloader" class="nojs"><div class="spinner"><div></div></div></div>
<div id="toggle-description" class="nojs active"><i class="fas fa-paragraph"></i></div>
<div id="content">
<div id="header">
<h1>
Explore {{ config.title }}
<span class="float-right">
{{ make_social_links(config) }}
</span>
</h1>
</div>
<ul>
{% for post in posts %}
<li class="article primary-text{{ ' hidden' if loop.index > 10 else '' }}">
<div class="article-banner" style="background-image: url('{{ post.banner_url(config) }}');">
<a href="{{ post.slug }}/" class="article-content">
<h2>{{ post.title }}</h2>
<p>
<i class="fas fa-map-marker-alt"></i>{{ post.location.title }}
<i class="far fa-calendar-alt"></i>{{ post.date }}
</p>
</a>
</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>

View File

@ -1,100 +1,43 @@
<!DOCTYPE html>
{% from "macros.html.j2" import make_header %}{% from "macros.html.j2" import make_social_links %}<!DOCTYPE html>
<html lang="en">
<head>
<!-- Web crawler and search indexing meta -->
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="author" content="admin@allaroundhere.org"/>
<meta name="description" content="Some of the best places are all around here"/>
<meta name="robots" content="index follow"/>
<meta
name="keywords"
content="travel photography explore exploration urbex urban nature all around here local museum history historical society"
/>
<!-- Facebook integration meta -->
<meta property="og:title" content="All Around Here"/>
<meta property="og:url" content="https://allaroundhere.org/explore/"/>
<meta property='og:site_name' content="All Around Here"/>
<meta property="og:type" content="website"/>
<meta property='og:locale' content="en_US"/>
<meta property="og:image" content="https://cdn.enp.one/img/backgrounds/cl-photo-boston.jpg"/>
<meta property='og:description' content="Some of the best places are all around here"/>
<!-- Twitter integration meta -->
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="https://allaroundhere.org/explore/">
<meta name="twitter:title" content="All Around Here">
<meta name="twitter:description" content="Some of the best places are all around here">
<meta name="twitter:image" content="https://cdn.enp.one/img/backgrounds/cl-photo-boston.jpg">
<meta name="twitter:image:alt" content="All Around Here">
<title>Explore All Around Here</title>
<link rel="shortcut icon" href="https://cdn.enp.one/img/logos/aah-b-sm.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.enp.one/img/logos/aah-b-sm.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://cdn.enp.one/img/logos/aah-b-sm.png" >
<link rel="icon" type="image/png" sizes="16x16" href="https://cdn.enp.one/img/logos/aah-b-sm.png">
<link rel="stylesheet" href="../css/explore.css"/>
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
crossorigin="anonymous"
/>
<script type="text/javascript" src="../js/custom.js"></script>
<noscript><style>.nojs { display: none; }</style></noscript>
</head>
{{ make_header(config, css_bundle=css_bundle, js_bundle=js_bundle) }}
<body>
<div id="background-image"><div class="overlay"></div></div>
<div id="background-image"></div>
<div id="preloader" class="nojs"><div class="spinner"><div></div></div></div>
<div id="toggle-description" class="nojs"><i class="fas fa-paragraph"></i></div>
<div id="content">
<div id="header">
<h1>
Explore All Around Here
<img
id="logo"
alt="Road to the great wide nowhere"
src="https://cdn.enp.one/img/logos/aah-md.jpg"
/>
<span class="float-right">
<a
class="button instagram"
title="Follow All Around Here on instagram @allaroundhere"
href="https://www.instagram.com/allaroundhere/"
>
<i class="fab fa-instagram"></i>
</a>
<a
class="button twitter"
title="Follow me on twitter @enpaul_"
href="https://www.twitter.com/enpaul_/"
>
<i class="fab fa-twitter"></i>
</a>
</span>
</h1>
</div>
<h1>{{ config.title }}</h1>
<ul>
{% for post in config.posts %}
<li class="article">
<div class="article-banner" style="background-image: url('{{ post.banner }}');">
<a href="{{ post.slug }}/" class="article-content">
<h2>{{ post.title }}</h2>
<p>
<i class="fas fa-map-marker-alt"></i>{{ post.location.title }}
<i class="far fa-calendar-alt"></i>{{ post.date }}
</p>
</a>
</div>
<p>
This is a project of mine where I turn my random travels, undirected wanderings, and
unexpected discoveries into something other people can enjoy along with me. There are a
lot of cool things in the world and I like to find them, wherever I happen to be. If you're
interested in seeing some of these arbitrary oddities then check out the links below.
</p>
<ul class="buttons">
<li>
<a class="button nav" title="Explore All Around Here" href="{{ config.baseurl }}{{ config.build.post_base }}">
<i class="fas fa-binoculars"></i>&nbsp;Explore
</a>
</li>
{% endfor %}
</ul>
<footer>
<div>
{{ make_social_links(config) }}
</div>
<div>
<a title="Personal website" href="https://enpaul.net/">&copy;2022-{{ today.year }} enpaul</a>
</div>
</footer>
</div>
</body>

62
templates/macros.html.j2 Normal file
View File

@ -0,0 +1,62 @@
{% macro make_header(config, alttitle=none, css_bundle=none, js_bundle=none) %}
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<!-- Web crawler and search indexing meta -->
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="author" content="{{ config.email }}"/>
<meta name="description" content="{{ config.description }}"/>
<meta name="robots" content="index follow"/>
<meta name="keywords" content="{{ config.keywords | join(' ') }}"/>
<!-- Facebook integration meta -->
<meta property="og:title" content="{{ alttitle or config.title }}"/>
<meta property="og:url" content="{{ config.url }}{{ config.build.post_base }}"/>
<meta property='og:site_name' content="{{ config.title }}"/>
<meta property="og:type" content="website"/>
<meta property='og:locale' content="en_US"/>
<meta property="og:image" content="https://cdn.enp.one/img/backgrounds/cl-photo-boston.jpg"/>
<meta property='og:description' content="{{ config.description }}"/>
<!-- Twitter integration meta -->
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="{{ config.url }}{{ config.build.post_base }}">
<meta name="twitter:title" content="{{ alttitle or config.title }}">
<meta name="twitter:description" content="{{ config.description }}">
<meta name="twitter:image" content="https://cdn.enp.one/img/backgrounds/cl-photo-boston.jpg">
<meta name="twitter:image:alt" content="{{ config.title }}">
<title>{{ alttitle or config.title }}</title>
<link rel="shortcut icon" href="https://cdn.enp.one/img/logos/aah-b-sm.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.enp.one/img/logos/aah-b-sm.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://cdn.enp.one/img/logos/aah-b-sm.png" >
<link rel="icon" type="image/png" sizes="16x16" href="https://cdn.enp.one/img/logos/aah-b-sm.png">
{% if css_bundle %}<link rel="stylesheet" href="{{ config.baseurl }}css/{{ css_bundle }}.css"/>{% endif %}
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
crossorigin="anonymous"
/>
{% if js_bundle %}<script type="text/javascript" src="{{ config.baseurl }}js/{{ js_bundle }}.js"></script>{% endif %}
<noscript><style>.nojs { display: none; }</style></noscript>
</head>
{% endmacro %}
{% macro make_social_links(config) %}
{% for social, link in config.social.items() %}
<a
class="button {{ social }}"
title="Follow me on {{ social }} @{{ link.rstrip('/').rpartition('/')[-1] }}"
href="{{ link }}"
>
<i class="fab fa-{{ social }}"></i>
</a>
{% endfor %}
{% endmacro %}

View File

@ -0,0 +1,43 @@
{% from "macros.html.j2" import make_header %}{% from "macros.html.j2" import make_social_links %}<!DOCTYPE html>
<html lang="en">
{{ make_header(config, alttitle=post.title, css_bundle=css_bundle, js_bundle=js_bundle) }}
<body>
<div id="preloader" class="nojs"><div class="spinner"><div></div></div></div>
{% for media in post.media %}
<div class="post-media" style="background-image: url('{{ media.preload_url(config) }}')"><div class="overlay"></div></div>
{% endfor %}
<div id="download-source" class="nojs"><i class="fas fa-cloud-download-alt"></i></div>
<div id="link" class="nojs"><i class="fas fa-link"></i></div>
<div id="media-previous" class="nojs"><i class="fas fa-chevron-left"></i></div>
<div id="media-next" class="nojs"><i class="fas fa-chevron-right"></i></div>
<div id="content">
<div id="header">
<h1>
Explore {{ config.title }}
<span class="float-right">
{{ make_social_links(config) }}
</span>
</h1>
</div>
<ul>
{% for post in posts %}
<li class="article primary-text">
<div class="article-banner" style="background-image: url('{{ post.banner_url(config) }}');">
<a href="{{ post.slug }}/" class="article-content">
<h2>{{ post.title }}</h2>
<p>
<i class="fas fa-map-marker-alt"></i>{{ post.location.title }}
<i class="far fa-calendar-alt"></i>{{ post.date }}
</p>
</a>
</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>

7
templates/robots.txt.j2 Normal file
View File

@ -0,0 +1,7 @@
# Allow all bots
User-agent: *
# Disallow access to non-content directories{% for path in disallowed %}
Disallow: {{ path }}{% endfor %}
Sitemap: {{ config.url }}sitemap.xml

View File

@ -7,21 +7,22 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://allaroundhere.org/</loc>
<lastmod>2021-02-01T00:30:55+00:00</lastmod>
<priority>1.00</priority>
<loc>{{ config.url }}</loc>
<lastmod>{{ today.strftime('%Y-%m-%dT%H:%M:%S') }}+00:00</lastmod>
<priority>0.90</priority>
</url>
<url>
<loc>https://allaroundhere.org/explore/</loc>
<lastmod>2021-02-01T00:30:55+00:00</lastmod>
<priority>1.10</priority>
<loc>{{ config.url }}{{ config.build.post_base }}</loc>
<lastmod>{{ today.strftime('%Y-%m-%dT%H:%M:%S') }}+00:00</lastmod>
<priority>1.00</priority>
</url>
{% for post in config.posts %}
{% for post in config.posts %}
<url>
<loc>https://allaroundhere.org/explore/{{ post.slug }}/</loc>
<lastmod>2021-02-01T00:30:55+00:00</lastmod>
<priority>0.90</priority>
<loc>{{ config.url }}{{ config.build.post_base }}{{ post.slug }}</loc>
<lastmod>{{ today.strftime('%Y-%m-%dT%H:%M:%S') }}+00:00</lastmod>
<priority>0.80</priority>
</url>
{% endfor %}
{% endfor %}
</urlset>