Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/adr/0015-list-separately-deployed-works.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ADR-0015: List separately deployed works under furedea.com paths

- Status: Accepted
- Date: 2026-10-19

In the context of a works section that links to projects served under this domain, such as
`/effort-or-luck/`, by their own deployments rather than by this site's build, facing an
internal-link check that resolves every same-origin link against the local preview, we decided
for listing works in `src/data/works.ts` and skipping the paths of same-origin works in that
check, and against copying their builds into this site or serving them from subdomains, to keep
each project deploying from its own repository while linking from the home page, accepting that
those links are checked only after the projects deploy.
2 changes: 2 additions & 0 deletions scripts/verify_build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const requiredHtml = {
"執行 凱斗",
"ソフトウェア工学",
"https://furedea.com/ja/",
"https://furedea.com/effort-or-luck/",
"/en/",
"https://github.com/",
"https://x.com/",
Expand All @@ -55,6 +56,7 @@ const requiredHtml = {
"Kaito Shigyo",
"Software Engineering",
"https://furedea.com/en/",
"https://furedea.com/effort-or-luck/",
"/ja/",
"https://github.com/",
"https://x.com/",
Expand Down
69 changes: 69 additions & 0 deletions src/components/works_section.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
import type { Lang } from "@i18n/ui";
import { useTranslations, pickLocale } from "@i18n/utils";
import { works } from "@data/works";
import Section from "./section.astro";
import TagList from "./tag_list.astro";

interface Props {
lang: Lang;
}

const { lang } = Astro.props;
const t = useTranslations(lang);
---

<Section id="works" title={t("section.works")}>
<div class="card-grid works-grid">
{
works.map((work) => (
<a class="work-card" href={work.url}>
<h3>{pickLocale(work.title, lang)}</h3>
<p>{pickLocale(work.description, lang)}</p>
<TagList tags={work.tags} />
</a>
))
}
</div>
</Section>

<style>
.works-grid {
--card-min: 20rem;
}

.work-card {
display: block;
padding: 1.5rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
color: inherit;
text-decoration: none;
transition:
border-color 0.15s,
transform 0.15s;

&:hover {
border-color: var(--color-accent);
transform: translateY(-2px);
}
}

.work-card h3 {
color: var(--color-text-primary);
font-size: 1.125rem;
margin-bottom: 0.5rem;
transition: color 0.15s;
}

.work-card:hover h3 {
color: var(--color-accent);
}

.work-card p {
font-size: 0.9375rem;
color: var(--color-text-secondary);
margin-bottom: 1rem;
}
</style>
1 change: 1 addition & 0 deletions src/data/sections.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const HOME_SECTIONS = [
"about",
"research",
"works",
"publications",
"news",
"blog",
Expand Down
20 changes: 20 additions & 0 deletions src/data/works.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { LocalizedText } from "@i18n/ui";

interface Work {
title: LocalizedText;
description: LocalizedText;
url: string;
tags: string[];
}

export const works: Work[] = [
{
title: { ja: "努力か,運か", en: "Effort or Luck" },
description: {
ja: "同じ選択,違う運.100人のあなたが就活の1年を生きるブラウザゲーム.",
en: "Same choice, different luck: 100 versions of you live one job-hunting year. A browser game.",
},
url: "https://furedea.com/effort-or-luck/",
tags: ["Astro", "React", "TypeScript"],
},
];
4 changes: 4 additions & 0 deletions src/i18n/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ export const ui = {
"nav.home": "ホーム",
"nav.about": "概要",
"nav.research": "研究",
"nav.works": "作品",
"nav.publications": "業績",
"nav.news": "ニュース",
"nav.blog": "ブログ",
"nav.contact": "連絡先",
"section.about": "About",
"section.research": "Research",
"section.works": "Works",
"section.publications": "Publications",
"section.news": "News",
"section.blog": "Blog",
Expand All @@ -42,12 +44,14 @@ export const ui = {
"nav.home": "Home",
"nav.about": "About",
"nav.research": "Research",
"nav.works": "Works",
"nav.publications": "Publications",
"nav.news": "News",
"nav.blog": "Blog",
"nav.contact": "Contact",
"section.about": "About",
"section.research": "Research",
"section.works": "Works",
"section.publications": "Publications",
"section.news": "News",
"section.blog": "Blog",
Expand Down
2 changes: 2 additions & 0 deletions src/pages/[lang]/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import BaseLayout from "@layouts/base_layout.astro";
import HeroSection from "@components/hero_section.astro";
import AboutSection from "@components/about_section.astro";
import ResearchSection from "@components/research_section.astro";
import WorksSection from "@components/works_section.astro";
import PublicationsSection from "@components/publications_section.astro";
import NewsSection from "@components/news_section.astro";
import BlogSection from "@components/blog_section.astro";
Expand All @@ -19,6 +20,7 @@ type SectionComponent = (typeof sectionComponents)[HomeSectionId];
const sectionComponents = {
about: AboutSection,
research: ResearchSection,
works: WorksSection,
publications: PublicationsSection,
news: NewsSection,
blog: BlogSection,
Expand Down
10 changes: 10 additions & 0 deletions tests/e2e/internal_links.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ import { join } from "node:path";

import { expect, test, type APIRequestContext, type Page } from "@playwright/test";

import { works } from "../../src/data/works";

const DIST_DIR = "dist";
const SITE_ORIGIN = "https://furedea.com";
// Works served under furedea.com by their own deployments, outside this site's build.
const SEPARATELY_DEPLOYED_PATHS = works
.map((work) => new URL(work.url))
.filter((url) => url.origin === SITE_ORIGIN)
.map((url) => url.pathname);
const REFERENCE_SELECTOR = [
"a[href]",
"area[href]",
Expand Down Expand Up @@ -121,6 +128,9 @@ function toInternalReference(sourcePath: string, rawTarget: string): InternalRef
if (target.origin !== SITE_ORIGIN) {
return undefined;
}
if (SEPARATELY_DEPLOYED_PATHS.some((path) => target.pathname.startsWith(path))) {
return undefined;
}
return {
sourcePath,
rawTarget,
Expand Down
14 changes: 14 additions & 0 deletions tests/e2e/site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";

import { profile } from "../../src/data/profile";
import { site } from "../../src/data/site";
import { works } from "../../src/data/works";

const linkByLabel = Object.fromEntries(
profile.socialLinks.map((link) => [link.platform, link.url]),
Expand Down Expand Up @@ -57,6 +58,19 @@ test("publishes the profile email address", async ({ page }) => {
);
});

test("lists works under their own nav item, each card linking to the work", async ({ page }) => {
for (const lang of ["ja", "en"] as const) {
await page.goto(`/${lang}/`);

await expect(page.locator('.site-nav a[href$="#works"]')).toHaveCount(1);
for (const work of works) {
const card = page.locator(".work-card", { hasText: work.title[lang] });
await expect(card).toHaveAttribute("href", work.url);
await expect(card).toContainText(work.description[lang]);
}
}
});

test("renders news collection records", async ({ page }) => {
await page.goto("/ja/");

Expand Down
Loading