Compare commits
7 Commits
38fadf32f6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b2bb55a2b | |||
| 6a3d9afd4f | |||
| 2e276b8864 | |||
| 07ec8f2ab1 | |||
| 3301c6cfa5 | |||
| 71b871ed5c | |||
| f9f26dc584 |
@@ -29,39 +29,15 @@ const kSuiteApps: KSuiteApp[] = [
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "Thoughts",
|
name: "Thoughts",
|
||||||
shortDescription: "Microblogging platform",
|
shortDescription: "Federated microblogging platform",
|
||||||
description:
|
description:
|
||||||
"Nostalgic social platform with Frutiger Aero style. 128-char posts, custom CSS profiles.",
|
"Nostalgic social platform with Frutiger Aero style. 128-char posts, custom CSS profiles, and full ActivityPub federation — interoperable with Mastodon, Misskey, and Movies Diary.",
|
||||||
url: "https://thoughts.gabrielkaszewski.dev/",
|
url: "https://thoughts.gabrielkaszewski.dev/",
|
||||||
githubUrl: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
githubUrl: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
||||||
icon: "/images/thoughts.avif",
|
icon: "/images/thoughts.avif",
|
||||||
technologies: ["Rust", "Next.js", "Axum"],
|
technologies: ["Rust", "Next.js", "Axum", "ActivityPub", "PostgreSQL", "NATS"],
|
||||||
color: "from-cyan-400 to-blue-500",
|
color: "from-cyan-400 to-blue-500",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "K-Tuner",
|
|
||||||
shortDescription: "Instrument tuner",
|
|
||||||
description:
|
|
||||||
"Tune guitar, ukulele, and piano with this Frutiger Aero styled PWA.",
|
|
||||||
url: "https://tuner.gabrielkaszewski.dev/",
|
|
||||||
githubUrl: "https://github.com/GKaszewski/k-tuner",
|
|
||||||
icon: "/images/k-tuner.png",
|
|
||||||
technologies: ["React", "PWA"],
|
|
||||||
color: "from-emerald-400 to-teal-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "K-QR",
|
|
||||||
shortDescription: "QR code generator",
|
|
||||||
description:
|
|
||||||
"High-performance QR generator. Single Rust executable serving clean HTML.",
|
|
||||||
url: "https://qr.gabrielkaszewski.dev/",
|
|
||||||
githubUrl: "https://github.com/GKaszewski/k-qr",
|
|
||||||
icon: "/images/k-qr.png",
|
|
||||||
technologies: ["Rust", "HTML"],
|
|
||||||
color: "from-amber-400 to-orange-500",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: "K-TV",
|
name: "K-TV",
|
||||||
@@ -91,15 +67,12 @@ const kSuiteApps: KSuiteApp[] = [
|
|||||||
// Connection definitions for the organism
|
// Connection definitions for the organism
|
||||||
const connections = [
|
const connections = [
|
||||||
{ from: 0, to: 1 }, // K-Notes -> Thoughts
|
{ from: 0, to: 1 }, // K-Notes -> Thoughts
|
||||||
{ from: 1, to: 2 }, // Thoughts -> K-Tuner
|
{ from: 1, to: 2 }, // Thoughts -> K-TV
|
||||||
{ from: 2, to: 3 }, // K-Tuner -> K-QR
|
{ from: 2, to: 3 }, // K-TV -> Movies Diary
|
||||||
{ from: 3, to: 4 }, // K-QR -> K-TV
|
{ from: 3, to: 0 }, // Movies Diary -> K-Notes
|
||||||
{ from: 4, to: 5 }, // K-TV -> Movies Diary
|
{ from: 0, to: 2 }, // K-Notes -> K-TV (cross)
|
||||||
{ from: 5, to: 0 }, // Movies Diary -> K-Notes
|
{ from: 1, to: 3 }, // Thoughts -> Movies Diary (AP)
|
||||||
{ from: 0, to: 2 }, // K-Notes -> K-Tuner (cross)
|
{ from: 3, to: 1 }, // Movies Diary -> Thoughts (federation)
|
||||||
{ from: 1, to: 3 }, // Thoughts -> K-QR (cross)
|
|
||||||
{ from: 2, to: 4 }, // K-Tuner -> K-TV (cross)
|
|
||||||
{ from: 5, to: 1 }, // Movies Diary -> Thoughts (federation)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const KSuiteOrganism = () => {
|
const KSuiteOrganism = () => {
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export const metadata: Metadata = {
|
|||||||
"Next.js",
|
"Next.js",
|
||||||
"Portfolio",
|
"Portfolio",
|
||||||
],
|
],
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://gabrielkaszewski.dev",
|
||||||
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Gabriel Kaszewski | Software Engineer",
|
title: "Gabriel Kaszewski | Software Engineer",
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
const MOVIE_REVIEWS_URL =
|
const MOVIE_REVIEWS_URL =
|
||||||
"https://movies.gabrielkaszewski.dev/users/5d253151-0f6a-4246-9bc5-cb0b5869731b";
|
"https://movies.gabrielkaszewski.dev/users/5d253151-0f6a-4246-9bc5-cb0b5869731b";
|
||||||
|
const MOVIE_REVIEWS_EMBED_URL =
|
||||||
|
"https://movies.gabrielkaszewski.dev/users/5d253151-0f6a-4246-9bc5-cb0b5869731b?embed=true";
|
||||||
|
|
||||||
const genres = ["Sci-Fi", "Drama", "Family"];
|
const genres = ["Sci-Fi", "Drama", "Family"];
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ export default function MovieCornerPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex-1 overflow-hidden rounded-md flex flex-col">
|
<div className="flex-1 overflow-hidden rounded-md flex flex-col">
|
||||||
<iframe
|
<iframe
|
||||||
src={MOVIE_REVIEWS_URL}
|
src={MOVIE_REVIEWS_EMBED_URL}
|
||||||
title="Gabriel's Movie Reviews"
|
title="Gabriel's Movie Reviews"
|
||||||
allow=""
|
allow=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.gabrielkaszewski.rule=Host(`gabrielkaszewski.dev`)"
|
- "traefik.http.routers.gabrielkaszewski.rule=Host(`gabrielkaszewski.dev`) || Host(`gabrielkaszewski.pl`)"
|
||||||
- "traefik.http.routers.gabrielkaszewski.entrypoints=websecure"
|
- "traefik.http.routers.gabrielkaszewski.entrypoints=websecure"
|
||||||
- "traefik.http.routers.gabrielkaszewski.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.gabrielkaszewski.tls.certresolver=letsencrypt"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
113
docs/superpowers/plans/2026-05-16-dual-domain.md
Normal file
113
docs/superpowers/plans/2026-05-16-dual-domain.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Dual-Domain Support Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Serve the portfolio on both `gabrielkaszewski.dev` and `gabrielkaszewski.pl` with identical content, with `.dev` as the canonical domain for SEO.
|
||||||
|
|
||||||
|
**Architecture:** Two independent changes — Traefik routing config to accept both hostnames, and Next.js metadata to inject a canonical `<link>` tag. No app logic changes.
|
||||||
|
|
||||||
|
**Tech Stack:** Next.js 15 (App Router metadata API), Traefik (Docker labels), Let's Encrypt TLS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add `.pl` domain to Traefik router
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `compose.yml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update the Traefik router rule**
|
||||||
|
|
||||||
|
In `compose.yml`, change the router rule label from:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.rule=Host(`gabrielkaszewski.dev`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.rule=Host(`gabrielkaszewski.dev`) || Host(`gabrielkaszewski.pl`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
The full `labels` block should look like:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.rule=Host(`gabrielkaszewski.dev`) || Host(`gabrielkaszewski.pl`)"
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.tls.certresolver=letsencrypt"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Traefik's Let's Encrypt cert resolver automatically provisions a cert for every hostname matched by the router rule, so no additional TLS labels are needed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add compose.yml
|
||||||
|
git commit -m "feat: add gabrielkaszewski.pl to traefik router"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add canonical URL to Next.js metadata
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/layout.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `alternates.canonical` to the metadata export**
|
||||||
|
|
||||||
|
In `app/layout.tsx`, add `alternates` to the existing `metadata` object:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
default: "Gabriel Kaszewski | Software Engineer",
|
||||||
|
template: "%s | Gabriel Kaszewski",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"The portfolio of Gabriel Kaszewski, a software engineer specializing in Rust, Python, and modern web technologies.",
|
||||||
|
keywords: [
|
||||||
|
"Gabriel Kaszewski",
|
||||||
|
"Software Engineer",
|
||||||
|
"Rust Developer",
|
||||||
|
"Python Developer",
|
||||||
|
"Next.js",
|
||||||
|
"Portfolio",
|
||||||
|
],
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://gabrielkaszewski.dev",
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
// ... rest unchanged
|
||||||
|
},
|
||||||
|
// ... rest unchanged
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify the canonical tag is present in the built HTML**
|
||||||
|
|
||||||
|
Run the dev server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in another terminal, check the rendered HTML for the canonical tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:3000 | grep -i canonical
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
|
||||||
|
```
|
||||||
|
<link rel="canonical" href="https://gabrielkaszewski.dev"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/layout.tsx
|
||||||
|
git commit -m "feat: add canonical URL to metadata"
|
||||||
|
```
|
||||||
38
docs/superpowers/specs/2026-05-16-dual-domain-design.md
Normal file
38
docs/superpowers/specs/2026-05-16-dual-domain-design.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Dual-Domain Support
|
||||||
|
|
||||||
|
**Date:** 2026-05-16
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Serve the portfolio under both `gabrielkaszewski.dev` and `gabrielkaszewski.pl` with identical content. The `.dev` domain is canonical for SEO.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 1. `compose.yml` — Traefik router rule
|
||||||
|
|
||||||
|
Extend the `Host()` rule to accept both domains and cover both with TLS:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.rule=Host(`gabrielkaszewski.dev`) || Host(`gabrielkaszewski.pl`)"
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gabrielkaszewski.tls.certresolver=letsencrypt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `app/layout.tsx` — canonical metadata
|
||||||
|
|
||||||
|
Add `alternates.canonical` to the exported `metadata` object:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://gabrielkaszewski.dev",
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
This injects `<link rel="canonical" href="https://gabrielkaszewski.dev">` into every page's `<head>`, telling search engines the `.dev` domain is authoritative.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- No redirects — `.pl` serves content directly.
|
||||||
|
- No per-domain content differences.
|
||||||
|
- No changes to OpenGraph or JSON-LD (already hardcoded to `.dev`).
|
||||||
@@ -185,14 +185,14 @@ export const projects: Project[] = [
|
|||||||
id: 6,
|
id: 6,
|
||||||
name: "Thoughts",
|
name: "Thoughts",
|
||||||
short_description:
|
short_description:
|
||||||
"Nostalgic microblogging platform with a Frutiger Aero aesthetic.",
|
"Federated microblogging platform with a Frutiger Aero aesthetic.",
|
||||||
description:
|
description:
|
||||||
"Thoughts is a microblogging social website straight out of the 00s, featuring a distinctive Frutiger Aero style. Users can post short text-based thoughts (up to 128 characters), customize their entire profile page with their own CSS, and follow others in a purely chronological, algorithm-free environment. It's a return to the 'old times' of the web, focusing on genuine interaction and user expression.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** Next.js",
|
"Thoughts is a fully federated microblogging platform built on ActivityPub, compatible with Mastodon, Misskey, Pleroma, and Movies Diary. It features a distinctive Frutiger Aero style straight out of the 00s.\n\nUsers can post short thoughts (up to 128 characters), reply, boost, and like. Profiles are fully customizable with user-supplied CSS. The feed is purely chronological — no algorithms.\n\n**Federation:**\n- WebFinger, NodeInfo, shared inbox, paginated outbox, and actor profile sync\n- Remote actor discovery by `@user@instance` handle\n- Follow/unfollow remote actors with proper `Accept`/`Follow` delivery\n- Per-domain and per-actor moderation with `Block` activity delivery\n\n**Technical details:**\n- **Architecture:** Hexagonal (Ports & Adapters)\n- **Backend:** Rust, Axum, SQLx\n- **Frontend:** Next.js, TailwindCSS\n- **Database:** PostgreSQL (full-text search via trigram indexes)\n- **Event bus:** NATS JetStream (async AP delivery and notifications)\n- **Auth:** JWT + API key\n- **Docs:** OpenAPI (Swagger UI + Scalar)",
|
||||||
category: "Web",
|
category: "Web",
|
||||||
github_url: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
github_url: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
||||||
visit_url: "https://thoughts.gabrielkaszewski.dev/",
|
visit_url: "https://thoughts.gabrielkaszewski.dev/",
|
||||||
download_url: null,
|
download_url: null,
|
||||||
technologies: ["Rust", "Next.js", "TailwindCSS", "Axum"],
|
technologies: ["Rust", "Axum", "Next.js", "TailwindCSS", "ActivityPub", "PostgreSQL", "NATS"],
|
||||||
thumbnails: ["/images/thoughts.avif"],
|
thumbnails: ["/images/thoughts.avif"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user