diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 02ea3f7..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "Node.js & TypeScript", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/typescript-node:0-20", - "waitFor": "onCreateCommand", - // Features to add to the dev container. More info: https://containers.dev/features. - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": "true", - "upgradePackages": "true" - }, - "ghcr.io/devcontainers/features/git:1": { - "version": "latest", - "ppa": "false" - } - }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [ - 3000 - ], - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "pnpm install", - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "streetsidesoftware.code-spell-checker", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "DavidAnson.vscode-markdownlint", - "ms-vscode-remote.remote-containers", - "ms-vscode.vscode-typescript-tslint-plugin" - ] - }, - "settings": { - "terminal.integrated.shell.linux": "/bin/zsh", - "typescript.validate.enable": false, - "eslint.alwaysShowStatus": true, - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - } - } - } - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - //"remoteUser": "vscode" -} \ No newline at end of file diff --git a/.devcontainer/postInstall.sh b/.devcontainer/postInstall.sh deleted file mode 100755 index 584f098..0000000 --- a/.devcontainer/postInstall.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# initalize project non-interactively -pnpm create-next-app@13.4.12 dotcom --use pnpm --ts --eslint --tailwind --no-src-dir --app --no-import-alias --example "https://github.com/vercel/next-learn/tree/main/basics/assets-metadata-css-starter" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..574bf19 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,15 @@ +name: Build + +on: + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + + - run: make build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e259036 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,25 @@ +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} + aws-region: us-east-1 + + - run: make deploy diff --git a/.gitignore b/.gitignore index a69b953..7964509 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,24 @@ -# Dependencies +# Build output +dist/ -# Production -/public +# Python +.venv/ +__pycache__/ +*.pyc +# Hugo (legacy — no longer used) +/public .hugo_build.lock -# Misc + +# Playwright +.playwright-mcp/ + +# Local Claude settings +.claude/settings.local.json + +# OS .DS_Store + +# Env +.env .env.local -.env.development.local -.env.test.local -.env.production.local diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 89af1b0..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "themes/PaperMod"] - path = themes/PaperMod - url = https://github.com/adityatelange/hugo-PaperMod.git diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c4265e --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +BUCKET := chrisdoescloud.com +STACK := chrisdoescloud-site +REGION := us-east-1 + +.PHONY: build deploy infra invalidate serve + +build: + uv run python build.py + +serve: build + uv run python -m http.server 8080 --directory dist + +infra: + sam deploy + +deploy: build + aws s3 sync dist/ s3://$(BUCKET) \ + --delete \ + --cache-control "public, max-age=31536000, immutable" \ + --exclude "*.html" + aws s3 sync dist/ s3://$(BUCKET) \ + --delete \ + --cache-control "public, max-age=0, must-revalidate" \ + --exclude "*" \ + --include "*.html" + $(MAKE) invalidate + +invalidate: + aws cloudfront create-invalidation \ + --distribution-id $$(aws cloudformation describe-stacks \ + --stack-name $(STACK) \ + --region $(REGION) \ + --query "Stacks[0].Outputs[?OutputKey=='DistributionId'].OutputValue" \ + --output text) \ + --paths "/*" diff --git a/about/index.md b/about/index.md new file mode 100644 index 0000000..cb04893 --- /dev/null +++ b/about/index.md @@ -0,0 +1,21 @@ +--- +title: About +--- + +I'm Chris Gonzalez — a cloud architect, terminal dweller, and occasional writer based in the US. + +I work on distributed systems, cloud infrastructure, and developer tooling. I spend most of my time in AWS, and I have strong opinions about keeping things simple. + +This site is where I put thoughts that don't fit in a Slack thread. + +## What I'm into + +- Cloud architecture and infrastructure as code +- AI-assisted development workflows (the kind that actually help) +- Spec-driven development and testable software +- Vim, the terminal, and tools that stay out of your way + +## Elsewhere + +- [GitHub](https://github.com/chrismgonzalez) +- [LinkedIn](https://linkedin.com/in/chrismgonzalez) diff --git a/archetypes/post.md b/archetypes/post.md deleted file mode 100644 index bdc2086..0000000 --- a/archetypes/post.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "My 1st post" -date: 2020-09-15T11:30:03+00:00 -# weight: 1 -# aliases: ["/first"] -tags: ["test-page"] -author: "Me" -# author: ["Me", "You"] # multiple authors -showToc: true -TocOpen: false -draft: false -hidemeta: false -comments: false -description: "Desc Text." -canonicalURL: "https://canonical.url/to/page" -disableHLJS: true # to disable highlightjs -disableShare: false -disableHLJS: false -hideSummary: false -searchHidden: true -ShowReadingTime: true -ShowBreadCrumbs: true -ShowPostNavLinks: true -ShowWordCount: true -ShowRssButtonInSectionTermList: true -UseHugoToc: true -cover: - image: "" # image path/url - alt: "" # alt text - caption: "" # display caption under cover - relative: false # when using page bundles set this to true - hidden: true # only hide on current single page -editPost: - URL: "https://github.com/chrismgonzalez/dotcom/content" - Text: "Suggest Changes" # edit text - appendFilePath: true # to append file path to Edit link ---- diff --git a/build.py b/build.py new file mode 100644 index 0000000..9c7d913 --- /dev/null +++ b/build.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +""" +Static site builder: markdown/ -> dist/ +Posts: posts/*.md -> dist/posts//index.html +About: about/index.md -> dist/about/index.html +Index: auto-generated -> dist/index.html +""" + +import os +import re +import shutil +from datetime import datetime +from pathlib import Path + +import markdown as md_lib + +ROOT = Path(__file__).parent +POSTS_DIR = ROOT / "posts" +ABOUT_DIR = ROOT / "about" +CV_DIR = ROOT / "cv" +DIST_DIR = ROOT / "dist" +STYLES_SRC = ROOT / "src" / "styles" / "main.css" +STATIC_SRC = ROOT / "static" + +SITE_TITLE = "ChrisDoesCloud" +CV_PUBLISHED = False # set in build() based on draft flag +SITE_DESC = "Ramblings of a clown architect" +BASE_URL = "" # empty = relative paths; set to https://chrisdoescloud.com for prod + + +# ── Frontmatter ────────────────────────────────────────────────────────────── + +def parse_frontmatter(text: str) -> tuple[dict, str]: + """Split YAML-ish frontmatter from body. Returns (meta, body).""" + meta = {} + if not text.startswith("---"): + return meta, text + end = text.index("---", 3) + fm_block = text[3:end].strip() + body = text[end + 3:].lstrip("\n") + for line in fm_block.splitlines(): + if ":" in line: + key, _, val = line.partition(":") + meta[key.strip()] = val.strip().strip('"').strip("'") + return meta, body + + +# ── Markdown -> HTML ────────────────────────────────────────────────────────── + +_md = md_lib.Markdown(extensions=["fenced_code", "tables", "nl2br"]) + +def to_html(text: str) -> str: + _md.reset() + return _md.convert(text) + + +# ── Date parsing ────────────────────────────────────────────────────────────── + +def parse_date(raw: str) -> datetime: + for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"): + try: + return datetime.strptime(raw[:len(fmt) + 2].rstrip("Z"), fmt.rstrip("z")) + except ValueError: + continue + return datetime.min + + +def fmt_date(dt: datetime) -> str: + return dt.strftime("%B %-d, %Y") + + +# ── Templates ───────────────────────────────────────────────────────────────── + +def _shell(title: str, canonical: str, body: str, active_nav: str = "") -> str: + def nav_link(href: str, label: str, key: str) -> str: + cls = ' class="active"' if key == active_nav else "" + return f'{label}' + + return f""" + + + + + {title + " — " + SITE_TITLE if title != SITE_TITLE else SITE_TITLE} + + + + + + + +
+
+ {SITE_TITLE} + +
+
+
+ {body} +
+ + +""" + + +def render_index(posts: list[dict]) -> str: + items = "" + for p in posts: + excerpt = p.get("excerpt", "") + items += f""" +
  • + +

    {p["title"]}

    + {"

    " + excerpt + "

    " if excerpt else ""} +
  • """ + + body = f""" +
      {items} +
    """ + return _shell(SITE_TITLE, "/", body, active_nav="index") + + +def render_post(meta: dict, html_body: str, slug: str) -> str: + title = meta.get("title", slug) + date_fmt = fmt_date(parse_date(meta.get("date", ""))) if meta.get("date") else "" + date_line = f'' if date_fmt else "" + body = f""" + ← All posts +
    +
    + {date_line} +

    {title}

    +
    +
    + {html_body} +
    +
    """ + return _shell(title, f"/posts/{slug}.html", body) + + +def render_about(html_body: str) -> str: + body = f""" +

    About

    +

    Hey, I'm Chris.

    +
    + {html_body} +
    """ + return _shell("About", "/about.html", body, active_nav="about") + + +def render_cv(html_body: str) -> str: + body = f""" +
    + {html_body} +
    """ + return _shell("CV", "/cv.html", body, active_nav="cv") + + +# ── Excerpt extraction ──────────────────────────────────────────────────────── + +def make_excerpt(body: str, words: int = 30) -> str: + text = re.sub(r"#+\s.*", "", body) + text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) + text = re.sub(r"`[^`]+`", "", text) + text = re.sub(r"\*+", "", text) + tokens = text.split()[:words] + return " ".join(tokens).rstrip(".,;:") + "…" if tokens else "" + + +# ── Build ───────────────────────────────────────────────────────────────────── + +def build(): + # Clean dist + if DIST_DIR.exists(): + shutil.rmtree(DIST_DIR) + DIST_DIR.mkdir() + + # Copy styles + styles_dir = DIST_DIR / "styles" + styles_dir.mkdir() + shutil.copy(STYLES_SRC, styles_dir / "main.css") + + # Copy static assets if present + if STATIC_SRC.exists(): + for item in STATIC_SRC.iterdir(): + dest = DIST_DIR / item.name + if item.is_dir(): + shutil.copytree(item, dest) + else: + shutil.copy(item, dest) + + posts = [] + + # Build posts + posts_out = DIST_DIR / "posts" + posts_out.mkdir() + + for md_file in sorted(POSTS_DIR.glob("*.md"), reverse=True): + text = md_file.read_text(encoding="utf-8") + meta, body = parse_frontmatter(text) + + if meta.get("draft", "false").lower() == "true": + continue + + slug = md_file.stem + html_body = to_html(body) + dt = parse_date(meta.get("date", "")) + + (posts_out / f"{slug}.html").write_text( + render_post(meta, html_body, slug), encoding="utf-8" + ) + + posts.append({ + "slug": slug, + "title": meta.get("title", slug), + "date": dt, + "date_fmt": fmt_date(dt) if meta.get("date") else "", + "excerpt": make_excerpt(body), + }) + print(f" post: /posts/{slug}.html") + + # Sort posts newest-first + posts.sort(key=lambda p: p["date"], reverse=True) + + # Build about + about_md = ABOUT_DIR / "index.md" + if about_md.exists(): + text = about_md.read_text(encoding="utf-8") + _, body = parse_frontmatter(text) + (DIST_DIR / "about.html").write_text(render_about(to_html(body)), encoding="utf-8") + print(" page: /about.html") + + # Build CV + global CV_PUBLISHED + cv_md = CV_DIR / "index.md" + if cv_md.exists(): + text = cv_md.read_text(encoding="utf-8") + meta, body = parse_frontmatter(text) + if meta.get("draft", "false").lower() != "true": + CV_PUBLISHED = True + (DIST_DIR / "cv.html").write_text(render_cv(to_html(body)), encoding="utf-8") + print(" page: /cv.html") + else: + print(" skip: /cv.html (draft)") + + # Build index + (DIST_DIR / "index.html").write_text(render_index(posts), encoding="utf-8") + print(" page: /") + + print(f"\nBuild complete -> {DIST_DIR}/") + + +if __name__ == "__main__": + build() diff --git a/build.sh b/build.sh deleted file mode 100644 index b975939..0000000 --- a/build.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# @file -# Builds a Hugo site hosted on Vercel. -# -# The Vercel build image automatically installs Node.js dependencies. -#------------------------------------------------------------------------------ - -main() { - - DART_SASS_VERSION=1.93.2 - GO_VERSION=1.25.3 - HUGO_VERSION=0.152.2 - NODE_VERSION=22.20.0 - - export TZ=Europe/Oslo - - # Install Dart Sass - echo "Installing Dart Sass ${DART_SASS_VERSION}..." - curl -sLJO "https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz" - tar -C "${HOME}/.local" -xf "dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz" - rm "dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz" - export PATH="${HOME}/.local/dart-sass:${PATH}" - - # Install Go - echo "Installing Go ${GO_VERSION}..." - curl -sLJO "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" - tar -C "${HOME}/.local" -xf "go${GO_VERSION}.linux-amd64.tar.gz" - rm "go${GO_VERSION}.linux-amd64.tar.gz" - export PATH="${HOME}/.local/go/bin:${PATH}" - - # Install Hugo - echo "Installing Hugo ${HUGO_VERSION}..." - curl -sLJO "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" - mkdir "${HOME}/.local/hugo" - tar -C "${HOME}/.local/hugo" -xf "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" - rm "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" - export PATH="${HOME}/.local/hugo:${PATH}" - - # Install Node.js - echo "Installing Node.js ${NODE_VERSION}..." - curl -sLJO "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" - tar -C "${HOME}/.local" -xf "node-v${NODE_VERSION}-linux-x64.tar.xz" - rm "node-v${NODE_VERSION}-linux-x64.tar.xz" - export PATH="${HOME}/.local/node-v${NODE_VERSION}-linux-x64/bin:${PATH}" - - # Verify installations - echo "Verifying installations..." - echo Dart Sass: "$(sass --version)" - echo Go: "$(go version)" - echo Hugo: "$(hugo version)" - echo Node.js: "$(node --version)" - - # Configure Git - echo "Configuring Git..." - git config core.quotepath false - if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then - git fetch --unshallow - fi - - # Build the site - echo "Building the site" - hugo --gc --minify --baseURL "https://${VERCEL_PROJECT_PRODUCTION_URL}" - -} - -set -euo pipefail -main "$@" diff --git a/cv/index.md b/cv/index.md new file mode 100644 index 0000000..4bfb590 --- /dev/null +++ b/cv/index.md @@ -0,0 +1,62 @@ +--- +title: CV +draft: true +--- + +## Chris Gonzalez + +Cloud Architect · AWS · Infrastructure · AI Tooling + +--- + +## Experience + +### Senior Cloud Architect — Caylent +*2022 – Present* + +Leading cloud infrastructure design and delivery for enterprise clients across AWS. Focused on platform engineering, IaC automation, and integrating AI-assisted development workflows into delivery teams. + +- Designed and delivered multi-account AWS landing zones using Control Tower and Terraform +- Built internal developer platforms reducing onboarding time from weeks to days +- Introduced spec-driven development practices across multiple client engagements +- Advised on AI tooling adoption including Claude Code, Kiro, and Cursor + +### Cloud Engineer — Previous Role +*2019 – 2022* + +Hands-on infrastructure engineering across AWS services. Responsible for CI/CD pipelines, container orchestration, and cloud cost optimization. + +- Migrated legacy monoliths to containerized microservices on ECS and EKS +- Reduced cloud spend by 35% through rightsizing and reserved instance planning +- Built GitOps workflows with ArgoCD and GitHub Actions + +--- + +## Skills + +### Cloud & Infrastructure +AWS (ECS, EKS, Lambda, RDS, CloudFront, S3, IAM, Control Tower), Terraform, CDK, SAM, Pulumi + +### Developer Tooling +GitHub Actions, ArgoCD, Docker, Kubernetes, Datadog, OpenTelemetry + +### Languages +Python, Bash, TypeScript, HCL + +### AI & Tooling +Claude Code, Kiro, Cursor, prompt engineering, spec-driven development + +--- + +## Education + +### B.S. Computer Science +*University Name · Year* + +--- + +## Certifications + +- AWS Certified Solutions Architect – Professional +- AWS Certified DevOps Engineer – Professional +- AWS Certified Security – Specialty diff --git a/hugo.yaml b/hugo.yaml deleted file mode 100644 index 6976264..0000000 --- a/hugo.yaml +++ /dev/null @@ -1,137 +0,0 @@ -baseURL: https://chridoescloud.com/ -languageCode: en-us -title: ChrisDoesCloud -theme: ["PaperMod"] - -enableRobotsTXT: true -buildDrafts: false -buildFuture: false -buildExpired: false - -# googleAnalytics: UA-123-45 - -minify: - disableXML: true - minifyOutput: true - -params: - env: production # to enable google analytics, opengraph, twitter-cards and schema. - title: ChrisDoesCloud - description: "Ramblings of a clown architect" - keywords: [Blog, Portfolio, PaperMod] - author: Me - # author: ["Me", "You"] # multiple authors - # images: [""] - DateFormat: "January 2, 2006" - defaultTheme: auto # dark, light - disableThemeToggle: false - - ShowReadingTime: true - ShowShareButtons: true - ShowPostNavLinks: true - ShowBreadCrumbs: true - ShowCodeCopyButtons: false - ShowWordCount: true - ShowRssButtonInSectionTermList: true - UseHugoToc: true - disableSpecial1stPost: false - disableScrollToTop: false - comments: false - hidemeta: false - hideSummary: false - showtoc: false - tocopen: false - - assets: - # disableHLJS: true # to disable highlight.js - # disableFingerprinting: true - favicon: "" - favicon16x16: "" - favicon32x32: "" - apple_touch_icon: "" - safari_pinned_tab: "" - - label: - text: "Home" - icon: /apple-touch-icon.png - iconHeight: 35 - - # profile-mode - profileMode: - enabled: true # needs to be explicitly set - title: ChrisDoesCloud - subtitle: "Welcome to ChrisDoesCloud, where we discuss cloud engineering, architecture, and best practices!" - # imageUrl: "" - # imageWidth: 120 - # imageHeight: 120 - # imageTitle: my image - buttons: - - name: Posts - url: posts - # - name: Tags - # url: tags - - # home-info mode - homeInfoParams: - Title: "Hi there \U0001F44B" - Content: Welcome to ChrisDoesCloud, where we discuss cloud engineering, architecture, and best practices! - - socialIcons: - - name: linkedin - url: "https://linkedin.com/in/cmgonzalez89" - - name: github - url: "https://github.com/chrismgonzalez" - - # analytics: - # google: - # SiteVerificationTag: "XYZabc" - # bing: - # SiteVerificationTag: "XYZabc" - # yandex: - # SiteVerificationTag: "XYZabc" - - cover: - hidden: true # hide everywhere but not in structured data - hiddenInList: true # hide on list pages and home - hiddenInSingle: true # hide on single page - - editPost: - URL: "https://github.com/chrismgonzalez/dotcom/content" - Text: "Suggest Changes" # edit text - appendFilePath: true # to append file path to Edit link - - # for search - # https://fusejs.io/api/options.html - fuseOpts: - isCaseSensitive: false - shouldSort: true - location: 0 - distance: 1000 - threshold: 0.4 - minMatchCharLength: 0 - limit: 10 # refer: https://www.fusejs.io/api/methods.html#search - keys: ["title", "permalink", "summary", "content"] -# menu: -# main: -# - identifier: about -# name: About -# url: /about/ -# weight: 10 -# - identifier: tags -# name: tags -# url: /tags/ -# weight: 20 -# - identifier: example -# name: example.org -# url: https://example.org -# weight: 30 -# Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma -pygmentsUseClasses: true -markup: - highlight: - noClasses: false - anchorLineNos: true - codeFences: true - guessSyntax: true - lineNos: true - style: monokai diff --git a/content/posts/kiro-ga-three-favorite-features.md b/posts/kiro-ga-three-favorite-features.md similarity index 100% rename from content/posts/kiro-ga-three-favorite-features.md rename to posts/kiro-ga-three-favorite-features.md diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..17d5888 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "dotcom" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.12" +dependencies = [ + "markdown>=3.10.2", +] diff --git a/samconfig.toml b/samconfig.toml new file mode 100644 index 0000000..8ac1fb0 --- /dev/null +++ b/samconfig.toml @@ -0,0 +1,9 @@ +version = 0.1 + +[default.deploy.parameters] +stack_name = "chrisdoescloud-site" +region = "us-east-1" +capabilities = "CAPABILITY_NAMED_IAM" +confirm_changeset = true +resolve_s3 = true +parameter_overrides = "DomainName=chrisdoescloud.com CertificateArn=arn:aws:acm:us-east-1:039624955047:certificate/20223716-09e1-4016-b35a-50eeb4318ad5" diff --git a/sidebars.ts b/sidebars.ts deleted file mode 100644 index acc7685..0000000 --- a/sidebars.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; - -/** - * Creating a sidebar enables you to: - - create an ordered group of docs - - render a sidebar for each doc of that group - - provide next/previous navigation - - The sidebars can be generated from the filesystem, or explicitly defined here. - - Create as many sidebars as you want. - */ -const sidebars: SidebarsConfig = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], - - // But you can create a sidebar manually - /* - tutorialSidebar: [ - 'intro', - 'hello', - { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], - }, - ], - */ -}; - -export default sidebars; diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..d454d6b --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,552 @@ +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap'); + +/* Catppuccin Mocha */ +:root { + --base: #1e1e2e; + --mantle: #181825; + --crust: #11111b; + --surface0:#313244; + --surface1:#45475a; + --surface2:#585b70; + --overlay0:#6c7086; + --overlay1:#7f849c; + --overlay2:#9399b2; + --subtext0:#a6adc8; + --subtext1:#bac2de; + --text: #cdd6f4; + --lavender:#b4befe; + --blue: #89b4fa; + --sapphire:#74c7ec; + --sky: #89dceb; + --teal: #94e2d5; + --green: #a6e3a1; + --yellow: #f9e2af; + --peach: #fab387; + --maroon: #eba0ac; + --red: #f38ba8; + --mauve: #cba6f7; + --pink: #f5c2e7; + --flamingo:#f2cdcd; + --rosewater:#f5e0dc; + + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --font-display: 'Playfair Display', Georgia, serif; + --font-body: var(--font-mono); + + --max-w: 720px; + --gap: 2rem; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + background: var(--base); + color: var(--text); + font-family: var(--font-body); + font-size: 0.9rem; + line-height: 1.75; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* ── Layout ── */ +.site-wrapper { + max-width: var(--max-w); + margin: 0 auto; + padding: 0 var(--gap); + width: 100%; + flex: 1; +} + +/* ── Header ── */ +header { + border-bottom: 1px solid var(--surface0); + padding: 2rem 0 1.5rem; + margin-bottom: 3rem; +} + +.site-header-inner { + max-width: var(--max-w); + margin: 0 auto; + padding: 0 var(--gap); + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.site-title { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 700; + color: var(--mauve); + text-decoration: none; + letter-spacing: -0.02em; +} + +.site-title:hover { color: var(--pink); } + +nav { + display: flex; + gap: 1.5rem; +} + +nav a { + color: var(--subtext1); + text-decoration: none; + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; + transition: color 0.15s; +} + +nav a:hover, nav a.active { color: var(--mauve); } + +/* ── Footer ── */ +footer { + border-top: 1px solid var(--surface0); + padding: 1.5rem 0; + margin-top: 4rem; +} + +.site-footer-inner { + max-width: var(--max-w); + margin: 0 auto; + padding: 0 var(--gap); + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +footer p { + font-size: 0.75rem; + color: var(--overlay0); +} + +footer a { + color: var(--overlay1); + text-decoration: none; +} + +footer a:hover { color: var(--mauve); } + +.site-footer-inner p:last-child { + display: flex; + gap: 1rem; +} + +/* ── Index / Post List ── */ +.page-eyebrow { + font-size: 0.7rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--mauve); + margin-bottom: 0.5rem; +} + +.page-title { + font-family: var(--font-display); + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 700; + color: var(--text); + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 3rem; +} + +.post-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 0; +} + +.post-list-item { + border-top: 1px solid var(--surface0); + padding: 1.5rem 0; +} + +.post-list-item:last-child { + border-bottom: 1px solid var(--surface0); +} + +.post-meta { + font-size: 0.72rem; + color: var(--overlay1); + letter-spacing: 0.04em; + margin-bottom: 0.4rem; +} + +.post-list-title { + font-family: var(--font-display); + font-size: 1.25rem; + font-weight: 700; + line-height: 1.2; + margin-bottom: 0.5rem; +} + +.post-list-title a { + color: var(--text); + text-decoration: none; + transition: color 0.15s; +} + +.post-list-title a:hover { color: var(--mauve); } + +.post-excerpt { + font-size: 0.82rem; + color: var(--subtext0); + line-height: 1.65; + max-width: 60ch; +} + +/* ── Post Page ── */ +.post-header { + margin-bottom: 2.5rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--surface0); +} + +.post-header .post-meta { + margin-bottom: 0.75rem; +} + +.post-header h1 { + font-family: var(--font-display); + font-size: clamp(1.8rem, 4vw, 2.5rem); + font-weight: 700; + color: var(--text); + line-height: 1.15; + letter-spacing: -0.025em; +} + +/* ── Prose ── */ +.prose { + color: var(--subtext1); + font-size: 0.9rem; + line-height: 1.8; +} + +.prose h2 { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 700; + color: var(--text); + margin: 2.5rem 0 0.75rem; + letter-spacing: -0.02em; +} + +.prose h3 { + font-family: var(--font-display); + font-size: 1.15rem; + font-weight: 700; + color: var(--lavender); + margin: 2rem 0 0.5rem; +} + +.prose p { margin-bottom: 1.25rem; } + +.prose a { + color: var(--blue); + text-decoration: underline; + text-decoration-color: var(--surface1); + text-underline-offset: 3px; + transition: color 0.15s, text-decoration-color 0.15s; +} + +.prose a:hover { + color: var(--mauve); + text-decoration-color: var(--mauve); +} + +.prose strong { color: var(--text); font-weight: 500; } + +.prose em { color: var(--flamingo); font-style: italic; } + +.prose ul, .prose ol { + padding-left: 1.5rem; + margin-bottom: 1.25rem; +} + +.prose li { margin-bottom: 0.4rem; } + +.prose ul li::marker { color: var(--mauve); } +.prose ol li::marker { color: var(--mauve); } + +.prose blockquote { + border-left: 2px solid var(--mauve); + padding: 0.5rem 0 0.5rem 1.25rem; + margin: 1.5rem 0; + color: var(--overlay2); + font-style: italic; +} + +.prose code { + background: var(--mantle); + color: var(--peach); + padding: 0.15em 0.4em; + border-radius: 3px; + font-size: 0.85em; + font-family: var(--font-mono); +} + +.prose pre { + background: var(--mantle); + border: 1px solid var(--surface0); + border-radius: 6px; + padding: 1.25rem; + overflow-x: auto; + margin: 1.5rem 0; +} + +.prose pre code { + background: none; + color: var(--text); + padding: 0; + font-size: 0.82rem; + line-height: 1.7; +} + +.prose hr { + border: none; + border-top: 1px solid var(--surface0); + margin: 2.5rem 0; +} + +/* ── About page ── */ +.about-intro { + font-family: var(--font-display); + font-size: 1.1rem; + font-style: italic; + color: var(--subtext1); + margin-bottom: 2.5rem; + line-height: 1.6; +} + +/* ── CV ── */ +.cv { + max-width: 65ch; +} + +.cv h2 { + font-family: var(--font-display); + font-size: 0.7rem; + font-weight: 400; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--mauve); + margin: 3rem 0 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--surface0); +} + +.cv h2:first-child { + font-family: var(--font-display); + font-size: clamp(1.6rem, 4vw, 2.2rem); + font-weight: 700; + letter-spacing: -0.025em; + text-transform: none; + color: var(--text); + border: none; + margin: 0 0 0.25rem; + padding: 0; +} + +.cv h3 { + font-family: var(--font-display); + font-size: 1.05rem; + font-weight: 700; + color: var(--text); + margin: 1.5rem 0 0; +} + +.cv p { + color: var(--subtext1); + font-size: 0.88rem; + line-height: 1.75; + margin-bottom: 0.75rem; +} + +/* Role subtitle / date line (em after h3) */ +.cv h3 + p > em:first-child { + display: block; + font-style: normal; + font-size: 0.72rem; + letter-spacing: 0.06em; + color: var(--overlay1); + margin-bottom: 0.5rem; +} + +.cv ul { + padding-left: 1.25rem; + margin-bottom: 1rem; +} + +.cv li { + font-size: 0.88rem; + color: var(--subtext1); + line-height: 1.65; + margin-bottom: 0.3rem; +} + +.cv li::marker { color: var(--mauve); } + +.cv hr { + border: none; + border-top: 1px solid var(--surface0); + margin: 2.5rem 0; +} + +.cv strong { + color: var(--text); + font-weight: 500; +} + +.cv a { + color: var(--blue); + text-decoration: none; +} + +.cv a:hover { color: var(--mauve); } + +/* ── Back link ── */ +.back-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.75rem; + color: var(--overlay1); + text-decoration: none; + letter-spacing: 0.06em; + text-transform: uppercase; + margin-bottom: 2rem; + transition: color 0.15s; +} + +.back-link:hover { color: var(--mauve); } + +/* ── Animations ── */ +@keyframes fade-up { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +header { animation: fade-up 0.4s ease both; } + +.page-title { animation: fade-up 0.4s 0.1s ease both; } + +.post-list { animation: fade-up 0.4s 0.1s ease both; } + +.post-list-item { + animation: fade-up 0.4s ease both; +} + +.post-list-item:nth-child(1) { animation-delay: 0.15s; } +.post-list-item:nth-child(2) { animation-delay: 0.22s; } +.post-list-item:nth-child(3) { animation-delay: 0.29s; } +.post-list-item:nth-child(4) { animation-delay: 0.36s; } +.post-list-item:nth-child(5) { animation-delay: 0.43s; } + +.post-header { animation: fade-up 0.4s 0.1s ease both; } +.prose { animation: fade-up 0.4s 0.2s ease both; } + +/* ── Responsive ── */ +@media (max-width: 640px) { + :root { + --gap: 1rem; + } + + html { + font-size: 15px; + } + + header { + padding: 1.25rem 0 1rem; + margin-bottom: 2rem; + } + + .site-title { + font-size: 1.2rem; + } + + nav { + gap: 1rem; + } + + nav a { + font-size: 0.75rem; + } + + .page-title { + font-size: clamp(1.6rem, 8vw, 2.2rem); + margin-bottom: 1.5rem; + } + + .post-list-item { + padding: 1.25rem 0; + } + + .post-list-title { + font-size: 1.1rem; + } + + .post-excerpt { + font-size: 0.8rem; + max-width: 100%; + } + + .post-header h1 { + font-size: clamp(1.5rem, 7vw, 2rem); + } + + .prose { + font-size: 0.875rem; + } + + .prose h2 { + font-size: 1.25rem; + } + + .prose h3 { + font-size: 1.05rem; + } + + /* Prevent code blocks from breaking layout */ + .prose pre { + padding: 1rem; + border-radius: 4px; + font-size: 0.78rem; + /* Pull to full width on mobile */ + margin-left: calc(-1 * var(--gap)); + margin-right: calc(-1 * var(--gap)); + border-left: none; + border-right: none; + border-radius: 0; + } + + .prose pre code { + font-size: 0.78rem; + } + + .prose ul, .prose ol { + padding-left: 1.25rem; + } + + .back-link { + margin-bottom: 1.5rem; + } + + footer { + margin-top: 3rem; + padding: 1.25rem 0; + } +} diff --git a/static/404.html b/static/404.html new file mode 100644 index 0000000..958d6f9 --- /dev/null +++ b/static/404.html @@ -0,0 +1,206 @@ + + + + + + 404 — chrisdoescloud.com + + + + + + + +
    +
    + + + + bash — chrisdoescloud.com +
    +
    + curl -s chrisdoescloud.com + + HTTP/2 404 Not Found + x-cache: Miss from CloudFront + x-amz-cf-id: y0uR3aLLy5ur3th1sIsWh3r3Y0uW4nt3dT0B3 + + ⚠ PageNotFoundError: The page you requested has been: + - accidentally rm -rf'd + - pushed to the wrong S3 bucket + - lost in a botched CloudFront invalidation + - never written in the first place (most likely) + + at CloudFront.serveOrigin (/edge/us-east-1/handler.js:404) + at S3.getObject (key=this-page-does-not-exist) + at clownArchitect.deploy (chrisdoescloud.com:1:1) + + cd /somewhere/that/exists +
    +
    + + + + + + + diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..dbdac94 --- /dev/null +++ b/template.yaml @@ -0,0 +1,143 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: chrisdoescloud.com static site - S3 + CloudFront + +Parameters: + DomainName: + Type: String + Default: chrisdoescloud.com + CertificateArn: + Type: String + Description: ACM certificate ARN (us-east-1) for chrisdoescloud.com and www.chrisdoescloud.com + +Resources: + + # ── S3 ────────────────────────────────────────────────────────────────────── + + SiteBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref DomainName + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + + SiteBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Ref SiteBucket + PolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: cloudfront.amazonaws.com + Action: s3:GetObject + Resource: !Sub "${SiteBucket.Arn}/*" + Condition: + StringEquals: + AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}" + + # ── CloudFront ─────────────────────────────────────────────────────────────── + + OriginAccessControl: + Type: AWS::CloudFront::OriginAccessControl + Properties: + OriginAccessControlConfig: + Name: !Sub "${DomainName}-oac" + OriginAccessControlOriginType: s3 + SigningBehavior: always + SigningProtocol: sigv4 + + Distribution: + Type: AWS::CloudFront::Distribution + Properties: + DistributionConfig: + Enabled: true + Aliases: + - !Ref DomainName + - !Sub "www.${DomainName}" + DefaultRootObject: index.html + HttpVersion: http2and3 + IPV6Enabled: true + PriceClass: PriceClass_100 + Origins: + - Id: S3Origin + DomainName: !GetAtt SiteBucket.RegionalDomainName + OriginAccessControlId: !Ref OriginAccessControl + S3OriginConfig: {} + DefaultCacheBehavior: + TargetOriginId: S3Origin + ViewerProtocolPolicy: redirect-to-https + CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized (AWS managed) + AllowedMethods: [GET, HEAD] + Compress: true + CustomErrorResponses: + - ErrorCode: 403 + ResponseCode: 404 + ResponsePagePath: /404.html + ErrorCachingMinTTL: 60 + ViewerCertificate: + AcmCertificateArn: !Ref CertificateArn + SslSupportMethod: sni-only + MinimumProtocolVersion: TLSv1.2_2021 + + # ── GitHub Actions deploy role ─────────────────────────────────────────────── + + GitHubActionsRole: + Type: AWS::IAM::Role + Properties: + RoleName: chrisdoescloud-github-actions + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com" + Action: sts:AssumeRoleWithWebIdentity + Condition: + StringEquals: + token.actions.githubusercontent.com:aud: sts.amazonaws.com + StringLike: + token.actions.githubusercontent.com:sub: repo:chrismgonzalez/dotcom:* + Policies: + - PolicyName: deploy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:PutObject + - s3:DeleteObject + - s3:ListBucket + Resource: + - !GetAtt SiteBucket.Arn + - !Sub "${SiteBucket.Arn}/*" + - Effect: Allow + Action: + - cloudfront:CreateInvalidation + Resource: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}" + - Effect: Allow + Action: + - cloudformation:DescribeStacks + Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*" + +Outputs: + BucketName: + Value: !Ref SiteBucket + Description: S3 bucket to sync dist/ into + + DistributionId: + Value: !Ref Distribution + Description: CloudFront distribution ID (needed for cache invalidation) + + DistributionDomain: + Value: !GetAtt Distribution.DomainName + Description: Add this as a CNAME in Cloudflare for both apex and www + + SiteUrl: + Value: !Sub "https://${DomainName}" + + GitHubActionsRoleArn: + Value: !GetAtt GitHubActionsRole.Arn + Description: Set this as AWS_DEPLOY_ROLE_ARN in GitHub repo secrets diff --git a/themes/PaperMod b/themes/PaperMod deleted file mode 160000 index 1cf5327..0000000 --- a/themes/PaperMod +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1cf53273c3ba58f0593ecb7c2befe11274f51a4e diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..bc70c63 --- /dev/null +++ b/uv.lock @@ -0,0 +1,23 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "dotcom" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "markdown" }, +] + +[package.metadata] +requires-dist = [{ name = "markdown", specifier = ">=3.10.2" }] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] diff --git a/vercel.json b/vercel.json deleted file mode 100644 index 4f492d5..0000000 --- a/vercel.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://openapi.vercel.sh/vercel.json", - "buildCommand": "chmod a+x build.sh && ./build.sh", - "outputDirectory": "public" -}