diff --git a/build.py b/build.py index 5d9edef..b6bca7c 100644 --- a/build.py +++ b/build.py @@ -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__": diff --git a/css/style.css b/bundle/css/common.css similarity index 56% rename from css/style.css rename to bundle/css/common.css index 35f5084..a1eea0f 100644 --- a/css/style.css +++ b/bundle/css/common.css @@ -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; } diff --git a/css/explore.css b/bundle/css/explore.css similarity index 76% rename from css/explore.css rename to bundle/css/explore.css index cbb27f5..9b77826 100644 --- a/css/explore.css +++ b/bundle/css/explore.css @@ -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; + } +} diff --git a/bundle/css/home.css b/bundle/css/home.css new file mode 100644 index 0000000..93a27f8 --- /dev/null +++ b/bundle/css/home.css @@ -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; + } +} diff --git a/bundle/js/preloader.js b/bundle/js/preloader.js new file mode 100644 index 0000000..d6d275a --- /dev/null +++ b/bundle/js/preloader.js @@ -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"; +}); diff --git a/js/custom.js b/bundle/js/random-background.js similarity index 69% rename from js/custom.js rename to bundle/js/random-background.js index 5ebd274..7892bc6 100644 --- a/js/custom.js +++ b/bundle/js/random-background.js @@ -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"; -}); diff --git a/bundle/js/toggle-article-text-button.js b/bundle/js/toggle-article-text-button.js new file mode 100644 index 0000000..0f9d9e4 --- /dev/null +++ b/bundle/js/toggle-article-text-button.js @@ -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); +}); diff --git a/error/400.html b/static/error/400.html similarity index 100% rename from error/400.html rename to static/error/400.html diff --git a/error/404.html b/static/error/404.html similarity index 100% rename from error/404.html rename to static/error/404.html diff --git a/error/500.html b/static/error/500.html similarity index 100% rename from error/500.html rename to static/error/500.html diff --git a/error/502.html b/static/error/502.html similarity index 100% rename from error/502.html rename to static/error/502.html diff --git a/templates/explore.html.j2 b/templates/explore.html.j2 new file mode 100644 index 0000000..f96668d --- /dev/null +++ b/templates/explore.html.j2 @@ -0,0 +1,40 @@ +{% from "macros.html.j2" import make_header %}{% from "macros.html.j2" import make_social_links %} + + {{ make_header(config, alttitle="Explore " + config.title, css_bundle=css_bundle, js_bundle=js_bundle) }} + +
+ +
+ +
+ +
+ + + +
+ + + diff --git a/templates/index.html.j2 b/templates/index.html.j2 index ca9cbc9..53a6ec6 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -1,100 +1,43 @@ - +{% from "macros.html.j2" import make_header %}{% from "macros.html.j2" import make_social_links %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - Explore All Around Here - - - - - - - - - - - - - + {{ make_header(config, css_bundle=css_bundle, js_bundle=js_bundle) }} -
+
-
-
- +

{{ config.title }}

-
diff --git a/templates/macros.html.j2 b/templates/macros.html.j2 new file mode 100644 index 0000000..579fffe --- /dev/null +++ b/templates/macros.html.j2 @@ -0,0 +1,62 @@ +{% macro make_header(config, alttitle=none, css_bundle=none, js_bundle=none) %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ alttitle or config.title }} + + + + + + {% if css_bundle %}{% endif %} + + + {% if js_bundle %}{% endif %} + + + +{% endmacro %} + +{% macro make_social_links(config) %} + {% for social, link in config.social.items() %} + + + + {% endfor %} +{% endmacro %} diff --git a/templates/post.html.j2 b/templates/post.html.j2 index e69de29..9d4067f 100644 --- a/templates/post.html.j2 +++ b/templates/post.html.j2 @@ -0,0 +1,43 @@ +{% from "macros.html.j2" import make_header %}{% from "macros.html.j2" import make_social_links %} + + {{ make_header(config, alttitle=post.title, css_bundle=css_bundle, js_bundle=js_bundle) }} + +
+ {% for media in post.media %} +
+ {% endfor %} +
+ +
+
+ +
+ + + +
+ + + diff --git a/templates/robots.txt.j2 b/templates/robots.txt.j2 new file mode 100644 index 0000000..64af470 --- /dev/null +++ b/templates/robots.txt.j2 @@ -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 diff --git a/templates/sitemap.xml.j2 b/templates/sitemap.xml.j2 index 7266112..2cb5916 100644 --- a/templates/sitemap.xml.j2 +++ b/templates/sitemap.xml.j2 @@ -7,21 +7,22 @@ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> - https://allaroundhere.org/ - 2021-02-01T00:30:55+00:00 - 1.00 + {{ config.url }} + {{ today.strftime('%Y-%m-%dT%H:%M:%S') }}+00:00 + 0.90 - https://allaroundhere.org/explore/ - 2021-02-01T00:30:55+00:00 - 1.10 + {{ config.url }}{{ config.build.post_base }} + {{ today.strftime('%Y-%m-%dT%H:%M:%S') }}+00:00 + 1.00 - {% for post in config.posts %} + +{% for post in config.posts %} - https://allaroundhere.org/explore/{{ post.slug }}/ - 2021-02-01T00:30:55+00:00 - 0.90 + {{ config.url }}{{ config.build.post_base }}{{ post.slug }} + {{ today.strftime('%Y-%m-%dT%H:%M:%S') }}+00:00 + 0.80 - {% endfor %} +{% endfor %}