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
6 changes: 6 additions & 0 deletions blog/services/post-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ ${'x'.repeat(5000)}
expect(await getFeedData(privateSlug)).toBeNull();
});

it('resolves percent-encoded slugs', () => {
const slug = '말하는-구조를-잃어버린-것-같았다';

expect(getFolderSlug(encodeURIComponent(slug))).toBe(slug);
});

it('returns null for non-existent posts folder slug', () => {
expect(getFolderSlug('missing-folder-slug')).toBeNull();
});
Expand Down
20 changes: 15 additions & 5 deletions blog/services/post-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const shouldLogContentIssues = process.env.NODE_ENV !== 'test';
const slugToFolderCache = new Map<string, string>();
let cachedSortedFeedData: FeedData[] | null = null;

function normalizeSlug(slug: string): string {
try {
return decodeURIComponent(slug);
} catch {
return slug;
}
}

function logContentIssue(message: string): void {
if (!shouldLogContentIssues) {
return;
Expand Down Expand Up @@ -252,15 +260,17 @@ function loadMetadata(folderPath: string): FeedFrontmatter | null {

// Get folder path from slug (using cache or scanning)
export function getFolderSlug(slug: string): string | null {
const normalizedSlug = normalizeSlug(slug);

// Check cache first
if (slugToFolderCache.has(slug)) {
return slugToFolderCache.get(slug)!;
if (slugToFolderCache.has(normalizedSlug)) {
return slugToFolderCache.get(normalizedSlug)!;
}

// Populate slug cache by loading full feed index first
getSortedFeedData({ includePrivate: true });
if (slugToFolderCache.has(slug)) {
return slugToFolderCache.get(slug)!;
if (slugToFolderCache.has(normalizedSlug)) {
return slugToFolderCache.get(normalizedSlug)!;
}

if (isProduction) {
Expand All @@ -272,7 +282,7 @@ export function getFolderSlug(slug: string): string | null {

for (const folderPath of allFolders) {
const metadata = loadMetadata(folderPath);
if (metadata?.slug === slug) {
if (metadata?.slug === normalizedSlug) {
return folderPath;
}
}
Expand Down
5 changes: 2 additions & 3 deletions site/shell/AppShell/AppShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ vi.mock('next/navigation', () => ({
}));

describe('AppShell', () => {
it('keeps Archive and Resume visible with external links in the identity rail', () => {
it('keeps primary navigation and external links available', () => {
const { container } = render(
<AppShell>
<main>content</main>
Expand All @@ -27,10 +27,9 @@ describe('AppShell', () => {
primaryNavigation.queryByRole('link', { name: 'Tech' })
).not.toBeInTheDocument();
expect(screen.getAllByLabelText('Ark 외부 링크')).toHaveLength(1);
expect(container.querySelector('.ark-site-identity')).toContainElement(
expect(container.querySelector('footer')).toContainElement(
screen.getByLabelText('Ark 외부 링크')
);
expect(container.querySelector('footer')).toBeNull();
expect(
container.querySelector('[data-page-layout="home"]')
).toBeInTheDocument();
Expand Down
5 changes: 4 additions & 1 deletion site/shell/AppShell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,14 @@ export default function AppShell({ children }: AppShellProps) {
ark
</Link>
</header>
<ExternalLinks />
</aside>

<div className="ark-site-content">{children}</div>

<footer className="ark-site-footer">
<ExternalLinks />
</footer>

<nav aria-label="Ark 주요 탐색" className="ark-site-navigation">
{PRIMARY_LINKS.map((item) => {
const isActive = item.isActive(pathname);
Expand Down
16 changes: 15 additions & 1 deletion styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@
min-width: 0;
}

.ark-site-footer {
grid-column: 1;
grid-row: 1;
position: sticky;
top: 2.5rem;
height: calc(100dvh - 5rem);
align-self: end;
Comment on lines +63 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep desktop external links pinned to the viewport rail

On content pages at widths of 640px and above, the footer shares row 1 with the article, so align-self: end places GitHub/Email/RSS at the bottom of the entire article-sized grid row. For long posts, the links therefore disappear from the desktop identity rail until the reader reaches the end, whereas the previous links were inside the viewport-height sticky identity element. Keep the footer sticky or otherwise preserve the prior desktop rail placement while applying the footer layout only on mobile.

AGENTS.md reference: AGENTS.md:L94-L95

Useful? React with 👍 / 👎.

}

.ark-site-footer .ark-site-external-links {
height: 100%;
justify-content: flex-end;
}

.ark-home-statement {
max-width: 34rem;
margin: 0;
Expand Down Expand Up @@ -164,7 +178,7 @@
.ark-site-external-links {
display: flex;
flex-direction: column;
margin-top: auto;
margin-top: 0;
pointer-events: auto;
}

Expand Down
2 changes: 1 addition & 1 deletion styles/globals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('globals styles', () => {

it('uses a compact single-column mobile shell and article entry scale', () => {
expect(mobileViewportContent).toContain(
"grid-template-areas:\n 'identity'\n 'navigation'\n 'content';"
"grid-template-areas:\n 'identity'\n 'navigation'\n 'content'\n 'footer';"
);
expect(globalsContent).toContain('.ark-article-title {');
expect(globalsContent).toContain('font-size: var(--text-article-title);');
Expand Down
55 changes: 53 additions & 2 deletions styles/viewport/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
grid-template-areas:
'identity'
'navigation'
'content';
'content'
'footer';
Comment on lines +12 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the mobile grid regression test

Adding the footer row here leaves styles/globals.test.ts:66-68 asserting the previous three-row declaration ending in 'content';. Running npm run test:unit -- styles/globals.test.ts now fails deterministically at that assertion, so the repository's full unit suite and test:ci cannot pass until the regression test is updated to reflect the intentional fourth row.

AGENTS.md reference: AGENTS.md:L74-L76

Useful? React with 👍 / 👎.

row-gap: var(--space-4);
min-height: auto;
}
Expand Down Expand Up @@ -38,6 +39,12 @@
grid-area: content;
}

.ark-site-footer {
grid-area: footer;
position: static;
height: auto;
}

.ark-site-navigation {
grid-area: navigation;
flex-direction: row;
Expand Down Expand Up @@ -77,16 +84,60 @@
}

.ark-site-grid[data-page-layout='home'] .ark-site-external-links {
grid-area: external;
display: flex;
flex-direction: column;
align-self: end;
justify-self: start;
gap: 0;
margin-top: 0;
}

.ark-site-grid[data-page-layout='home'] .ark-site-footer {
grid-area: external;
align-self: end;
justify-self: start;
margin-bottom: var(--space-8);
Comment on lines +95 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the duplicated mobile-home footer margin

On the mobile home layout, .ark-site-external-links still has margin-bottom: var(--space-8) while its new grid-item footer receives the same margin here. Because a grid item's margins do not collapse with its child's margin, the external links are shifted upward by 4rem instead of the previous 2rem, changing the home layout even though this change is intended to preserve it. Apply the bottom spacing to only the wrapper or the links.

AGENTS.md reference: AGENTS.md:L94-L95

Useful? React with 👍 / 👎.

}

.ark-site-grid[data-page-layout='content'] {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-areas:
'identity navigation'
'content content'
'footer footer';
column-gap: var(--space-4);
}

.ark-site-grid[data-page-layout='content'] .ark-site-navigation {
grid-area: navigation;
flex-direction: row;
align-self: center;
justify-self: end;
gap: var(--space-4);
padding-top: 0;
}

.ark-site-grid[data-page-layout='content'] .ark-site-content {
grid-area: content;
}

.ark-site-grid[data-page-layout='content'] .ark-site-footer {
grid-area: footer;
border-top: 1px solid var(--color-divider);
padding-top: var(--space-6);
padding-bottom: var(--space-2);
}

.ark-site-grid[data-page-layout='content'] .ark-site-footer .ark-site-external-links {
height: auto;
}

.ark-site-grid[data-page-layout='content'] .ark-site-external-links {
flex-direction: row;
justify-content: center;
gap: var(--space-4);
}

.ark-site-grid[data-page-layout='home'] .ark-site-content {
grid-area: content;
align-self: start;
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/smoke/home-renewal.smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ test.describe('Home and archive', () => {
}

expect(github.x).toBe(32);
expect(github.y).toBe(788);
expect(github.y).toBeGreaterThan(780);
}
});

Expand Down
10 changes: 5 additions & 5 deletions tests/e2e/smoke/mobile-nav.smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test.describe('Mobile navigation', () => {
await expect(page).toHaveURL(/\/archive/);
});

test('모바일 상단 컴팩트 영역에 보조 외부 링크를 둬요', async ({ page }) => {
test('모바일 본문 하단에 보조 외부 링크를 둬요', async ({ page }) => {
await page.goto('/archive');

const github = page.getByRole('link', { name: 'GitHub' });
Expand All @@ -44,14 +44,14 @@ test.describe('Mobile navigation', () => {

const githubBox = await github.boundingBox();
const emailBox = await email.boundingBox();
if (!githubBox || !emailBox) {
const footerBox = await page.locator('.ark-site-footer').boundingBox();
if (!githubBox || !emailBox || !footerBox) {
throw new Error(
'상단 컴팩트 영역의 외부 링크 위치를 측정할 수 없습니다.'
'본문 하단 footer의 외부 링크 위치를 측정할 수 없습니다.'
);
}

expect(githubBox.x).toBeGreaterThan(160);
expect(githubBox.y).toBeLessThan(120);
expect(githubBox.y).toBeGreaterThanOrEqual(footerBox.y);
expect(emailBox.y).toBe(githubBox.y);
});

Expand Down
Loading