diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7429811 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx +COPY _site /usr/share/nginx/html +COPY nginx /etc/nginx/conf.d \ No newline at end of file diff --git a/deploy/certificate.yaml b/deploy/certificate.yaml new file mode 100644 index 0000000..9d3d666 --- /dev/null +++ b/deploy/certificate.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: garrettmills-dev-tls + namespace: default +spec: + secretName: garrettmills-dev-tls-secret + dnsNames: + - 'garrettmills.dev' + - 'www.garrettmills.dev' + - 'k8s.garrettmills.dev' + issuerRef: + name: letsencrypt-ca + kind: ClusterIssuer diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml new file mode 100644 index 0000000..f3657b2 --- /dev/null +++ b/deploy/deployment.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: garrettmills-dev + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: garrettmills-dev + template: + metadata: + name: garrettmills-dev-www + namespace: default + labels: + app: garrettmills-dev + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: ['garrettmills-dev'] + topologyKey: 'kubernetes.io/hostname' + containers: + - name: garrettmills-www + image: registry.millslan.net/garrettmills/www-ssg + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 60 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 diff --git a/deploy/ingress.yaml b/deploy/ingress.yaml new file mode 100644 index 0000000..2c117ab --- /dev/null +++ b/deploy/ingress.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: garrettmills-dev-ingress + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: 'false' +spec: + tls: + - hosts: + - garrettmills.dev + - www.garrettmills.dev + - k8s.garrettmills.dev + secretName: garrettmills-dev-tls-secret + ingressClassName: nginx + rules: + - host: garrettmills.dev + http: + paths: + - pathType: Prefix + path: '/' + backend: + service: + name: garrettmills-dev-service + port: + number: 80 + - host: www.garrettmills.dev + http: + paths: + - pathType: Prefix + path: '/' + backend: + service: + name: garrettmills-dev-service + port: + number: 80 + - host: k8s.garrettmills.dev + http: + paths: + - pathType: Prefix + path: '/' + backend: + service: + name: garrettmills-dev-service + port: + number: 80 diff --git a/deploy/service.yaml b/deploy/service.yaml new file mode 100644 index 0000000..9bd3ee5 --- /dev/null +++ b/deploy/service.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: garrettmills-dev-service + namespace: default +spec: + selector: + app: garrettmills-dev + ports: + - port: 80 + targetPort: 80 diff --git a/eleventy.config.js b/eleventy.config.js index a71b61e..d6789bb 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -1,101 +1,39 @@ -import fetch from "node-fetch"; -import pugPlugin from "@11ty/eleventy-plugin-pug"; -import rssPlugin from "@11ty/eleventy-plugin-rss"; -import brokenLinksPlugin from "eleventy-plugin-broken-links"; -import { eleventyImageTransformPlugin } from "@11ty/eleventy-img"; -import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"; -import footnote from "markdown-it-footnote"; -import { EleventyRenderPlugin, IdAttributePlugin } from '@11ty/eleventy'; -import * as fs from 'fs'; -import * as opml from 'opml'; +import pugPlugin from '@11ty/eleventy-plugin-pug' +import rssPlugin from '@11ty/eleventy-plugin-rss' +import brokenLinksPlugin from 'eleventy-plugin-broken-links' +import { eleventyImageTransformPlugin } from '@11ty/eleventy-img' +import syntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight' +import footnote from 'markdown-it-footnote' +import { EleventyRenderPlugin, IdAttributePlugin } from '@11ty/eleventy' +import { setupBlogCollections } from './scripts/eleventy/blog.js' +import { setupFeedCollections } from './scripts/eleventy/feed.js' -const OUTLINE = { - url: 'https://outline.garrettmills.dev/api', - apiKey: 'ol_api_oyqeX4YSeUe8L1GKvrcGusYpHjyjIAtrSqUHJt', - documentId: '2abb5ebe-f856-4566-a21b-eab845f8dfdd', +const setupDirectories = eleventyConfig => { + eleventyConfig.setInputDirectory('src') + eleventyConfig.addPassthroughCopy('src/assets/**') + eleventyConfig.addPassthroughCopy('src/favicon.ico') } -export default function (eleventyConfig) { - eleventyConfig.setInputDirectory("src") - eleventyConfig.addPlugin(brokenLinksPlugin); - eleventyConfig.addPlugin(pugPlugin); - eleventyConfig.addPlugin(rssPlugin); - eleventyConfig.addPlugin(syntaxHighlight); +const setupPlugins = eleventyConfig => { + eleventyConfig.addPlugin(brokenLinksPlugin) + eleventyConfig.addPlugin(pugPlugin) + eleventyConfig.addPlugin(rssPlugin) + eleventyConfig.addPlugin(syntaxHighlight) + eleventyConfig.addPlugin(IdAttributePlugin) + eleventyConfig.addPlugin(EleventyRenderPlugin) + eleventyConfig.amendLibrary('md', md => md.use(footnote)) eleventyConfig.addPlugin(eleventyImageTransformPlugin, { htmlOptions: { imgAttributes: { alt: '', }, }, - }); - eleventyConfig.addPlugin(IdAttributePlugin); - eleventyConfig.addPlugin(EleventyRenderPlugin); - eleventyConfig.addPassthroughCopy("src/assets/**") - eleventyConfig.addPassthroughCopy("src/favicon.ico") - eleventyConfig.amendLibrary("md", md => md.use(footnote)) - eleventyConfig.addCollection("markmark", async api => { - const response = await fetch(`${OUTLINE.url}/documents.info`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${OUTLINE.apiKey}`, - }, - body: JSON.stringify({ - id: OUTLINE.documentId, - }), - }) - - if ( !response.ok ) { - throw new Error('Unable to load bookmarks from Outline: response was not okay') - } - - const body = await response.json() - console.log('body', body) - return [] - }) - - eleventyConfig.addCollection("blogByYear", api => { - const postsByYear = {} - const posts = api.getFilteredByTag('blog') - const years = [] - for ( const post of posts ) { - const year = post.date.getFullYear() - if ( !postsByYear[year] ) postsByYear[year] = [] - postsByYear[year] = [post, ...postsByYear[year]] - if ( !years.includes(year) ) years.push(year) - } - - return years.sort().reverse().map(year => ({ - year, - posts: postsByYear[year], - })) - }) - - eleventyConfig.addCollection("blogByTag", api => { - const postsByTag = {} - const posts = api.getFilteredByTag('blog') - const tags = [] - for ( const post of posts ) { - for ( const tag of post.data.blogtags || [] ) { - if ( !postsByTag[tag] ) postsByTag[tag] = [] - postsByTag[tag].push(post) - if ( !tags.includes(tag) ) tags.push(tag) - } - } - - return tags.sort().map(tag => ({ - tag, - posts: postsByTag[tag], - })) - }) - - eleventyConfig.addCollection("opmlByCategory", async api => { - const xml = fs.readFileSync("./src/assets/rss_opml.xml") - const parsed = await new Promise((res, rej) => { - opml.parse(xml, (err, doc) => err ? rej(err) : res(doc)) - }) - - return parsed.opml.body.subs }) } +export default function (eleventyConfig) { + setupPlugins(eleventyConfig) + setupDirectories(eleventyConfig) + setupBlogCollections(eleventyConfig) + setupFeedCollections(eleventyConfig) +} diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..f24115c --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name localhost; + absolute_redirect off; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/package.json b/package.json index 113453b..2639291 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,12 @@ "main": "index.js", "scripts": { "build": "pnpm run logos && eleventy --config eleventy.config.js", - "logos": "cd node_modules/canvas && pnpm run install && cd ../../ && node logo.js && node favicons.js" + "serve": "pnpm run logos && eleventy --serve", + "logos": "cd node_modules/canvas && pnpm run install && cd ../../ && node scripts/logo.js && node scripts/favicons.js", + "docker:build": "pnpm run build && docker build -t ${DOCKER_REGISTRY}/garrettmills/www-ssg .", + "docker:run": "pnpm run docker:build && docker run --rm -p 8080:80 ${DOCKER_REGISTRY}/garrettmills/www-ssg", + "docker:sh": "pnpm run docker:build && docker run --rm -it -p 8080:80 ${DOCKER_REGISTRY}/garrettmills/www-ssg sh", + "docker:push": "docker push ${DOCKER_REGISTRY}/garrettmills/www-ssg" }, "keywords": [], "author": "", diff --git a/scripts/eleventy/blog.js b/scripts/eleventy/blog.js new file mode 100644 index 0000000..32f2650 --- /dev/null +++ b/scripts/eleventy/blog.js @@ -0,0 +1,50 @@ +import fs from "fs"; +import * as opml from "opml"; + +export const setupBlogCollections = eleventyConfig => { + eleventyConfig.addCollection("blogByYear", api => { + const postsByYear = {} + const posts = api.getFilteredByTag('blog') + const years = [] + for ( const post of posts ) { + const year = post.date.getFullYear() + if ( !postsByYear[year] ) postsByYear[year] = [] + postsByYear[year] = [post, ...postsByYear[year]] + if ( !years.includes(year) ) years.push(year) + } + + return years.sort().reverse().map(year => ({ + year, + posts: postsByYear[year], + })) + }) + + eleventyConfig.addCollection("blogByTag", api => { + const postsByTag = {} + const posts = api.getFilteredByTag('blog') + const tags = [] + for ( const post of posts ) { + for ( const tag of post.data.blogtags || [] ) { + if ( !postsByTag[tag] ) postsByTag[tag] = [] + postsByTag[tag].push(post) + if ( !tags.includes(tag) ) tags.push(tag) + } + } + + return tags + .sort() + .map(tag => ({ + tag, + posts: postsByTag[tag].reverse(), + })) + }) + + eleventyConfig.addCollection("opmlByCategory", async api => { + const xml = fs.readFileSync("./src/assets/rss_opml.xml") + const parsed = await new Promise((res, rej) => { + opml.parse(xml, (err, doc) => err ? rej(err) : res(doc)) + }) + + return parsed.opml.body.subs + }) +} diff --git a/scripts/eleventy/feed.js b/scripts/eleventy/feed.js new file mode 100644 index 0000000..0332a00 --- /dev/null +++ b/scripts/eleventy/feed.js @@ -0,0 +1,10 @@ + +export const setupFeedCollections = eleventyConfig => { + eleventyConfig.addCollection("feedDesc", api => { + const posts = api.getFilteredByTag('feed') + return posts + .sort((a, b) => { + return b.date < a.date ? -1 : 1 + }) + }) +} diff --git a/favicons.js b/scripts/favicons.js similarity index 100% rename from favicons.js rename to scripts/favicons.js diff --git a/logo.js b/scripts/logo.js similarity index 100% rename from logo.js rename to scripts/logo.js diff --git a/src/_includes/feed_post.pug b/src/_includes/feed_post.pug index bf047fd..8c2ed8e 100644 --- a/src/_includes/feed_post.pug +++ b/src/_includes/feed_post.pug @@ -10,8 +10,4 @@ block content h2 #{title} .content-wrapper !{content} .footer - a.permalink(href=`/feed/#${slug}`) permalink (v1) - span.sep | - a.permalink(href=url) permalink (v2) - span.sep | span.date #{date.toLocaleDateString()} diff --git a/src/assets/css/feed.css b/src/assets/css/feed.css index c30cdde..7ff4c7e 100644 --- a/src/assets/css/feed.css +++ b/src/assets/css/feed.css @@ -28,6 +28,7 @@ .feed .post .footer { margin-top: 15px; + color: var(--color-2); } .feed .post .footer .sep { diff --git a/src/assets/rss_opml.xml b/src/assets/rss_opml.xml index 732ab36..5b3bc38 100644 --- a/src/assets/rss_opml.xml +++ b/src/assets/rss_opml.xml @@ -1,259 +1,289 @@ - - - - NewsFlash OPML export - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + Sat, 28 Feb 2026 15:51:52 GMT + garrettmills subscriptions in CommaFeed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/blog/feeds.njk b/src/blog/feeds.njk index 3911648..1adef4e 100644 --- a/src/blog/feeds.njk +++ b/src/blog/feeds.njk @@ -11,7 +11,11 @@ This list is also available in the standard {% for cat in collections.opmlByCategory %} -

{{ cat.title }}

+ {% if (cat.title == "Garrett Mills") %} +

{{ cat.title }} (Shameless Self-Plug)

+ {% else %} +

{{ cat.title }}

+ {% endif %}