Initialize blog scaffold
Add the CLI, site, and sample content so the project can run locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9d2628b318
commit
b78f4b39c9
40 changed files with 9140 additions and 0 deletions
6
frontend/site/astro.config.mjs
Normal file
6
frontend/site/astro.config.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'static',
|
||||
outDir: '../../dist/site'
|
||||
});
|
||||
5591
frontend/site/package-lock.json
generated
Normal file
5591
frontend/site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
17
frontend/site/package.json
Normal file
17
frontend/site/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@osaet/site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^4.0.12",
|
||||
"astro": "^5.0.0",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
14
frontend/site/src/components/SiteNav.astro
Normal file
14
frontend/site/src/components/SiteNav.astro
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
const links = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/archive/', label: 'Archive' },
|
||||
{ href: '/tags/', label: 'Tags' },
|
||||
{ href: '/rss.xml', label: 'RSS' }
|
||||
];
|
||||
---
|
||||
|
||||
<nav class="site-nav" aria-label="Primary">
|
||||
{links.map((link) => (
|
||||
<a href={link.href}>{link.label}</a>
|
||||
))}
|
||||
</nav>
|
||||
89
frontend/site/src/lib/posts.ts
Normal file
89
frontend/site/src/lib/posts.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
type MarkdownPost = {
|
||||
frontmatter: {
|
||||
slug: string;
|
||||
title: string;
|
||||
summary?: string;
|
||||
status?: string;
|
||||
tags?: string[];
|
||||
published_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Post = MarkdownPost['frontmatter'] & {
|
||||
url: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export type TagSummary = {
|
||||
name: string;
|
||||
slug: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type ArchiveYear = {
|
||||
year: string;
|
||||
posts: Post[];
|
||||
};
|
||||
|
||||
export function getPublishedPosts(): Post[] {
|
||||
const modules = import.meta.glob('../../../../content/posts/*.md', { eager: true });
|
||||
|
||||
return Object.values(modules)
|
||||
.map((post) => {
|
||||
const frontmatter = (post as MarkdownPost).frontmatter;
|
||||
return {
|
||||
...frontmatter,
|
||||
url: `/posts/${frontmatter.slug}/`,
|
||||
date: frontmatter.published_at ?? frontmatter.updated_at ?? '',
|
||||
tags: frontmatter.tags ?? []
|
||||
};
|
||||
})
|
||||
.filter((post) => post.status === 'published')
|
||||
.sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
||||
}
|
||||
|
||||
export function tagSlug(tag: string): string {
|
||||
return encodeURIComponent(tag.trim().toLowerCase().replace(/\s+/g, '-'));
|
||||
}
|
||||
|
||||
export function getTagSummaries(): TagSummary[] {
|
||||
const counts = new Map<string, TagSummary>();
|
||||
|
||||
for (const post of getPublishedPosts()) {
|
||||
for (const tag of post.tags) {
|
||||
const name = tag.trim();
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const slug = tagSlug(name);
|
||||
const current = counts.get(slug);
|
||||
if (current) {
|
||||
current.count += 1;
|
||||
} else {
|
||||
counts.set(slug, { name, slug, count: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...counts.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export function getPostsByTag(slug: string): Post[] {
|
||||
return getPublishedPosts().filter((post) => post.tags.some((tag) => tagSlug(tag) === slug));
|
||||
}
|
||||
|
||||
export function getArchiveYears(): ArchiveYear[] {
|
||||
const years = new Map<string, Post[]>();
|
||||
|
||||
for (const post of getPublishedPosts()) {
|
||||
const year = post.date ? String(new Date(post.date).getFullYear()) : 'Undated';
|
||||
years.set(year, [...(years.get(year) ?? []), post]);
|
||||
}
|
||||
|
||||
return [...years.entries()]
|
||||
.sort(([a], [b]) => b.localeCompare(a))
|
||||
.map(([year, posts]) => ({ year, posts }));
|
||||
}
|
||||
60
frontend/site/src/lib/siteConfig.ts
Normal file
60
frontend/site/src/lib/siteConfig.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import YAML from 'yaml';
|
||||
|
||||
const siteRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
||||
|
||||
type SiteConfig = {
|
||||
site: {
|
||||
title: string;
|
||||
description: string;
|
||||
base_url: string;
|
||||
language: string;
|
||||
timezone: string;
|
||||
};
|
||||
content: {
|
||||
posts_dir: string;
|
||||
assets_dir: string;
|
||||
};
|
||||
build: {
|
||||
astro_project: string;
|
||||
output_dir: string;
|
||||
};
|
||||
};
|
||||
|
||||
const defaults: SiteConfig = {
|
||||
site: {
|
||||
title: 'Osaet',
|
||||
description: 'Personal blog',
|
||||
base_url: 'http://localhost:4321',
|
||||
language: 'zh-CN',
|
||||
timezone: 'Asia/Shanghai'
|
||||
},
|
||||
content: {
|
||||
posts_dir: 'content/posts',
|
||||
assets_dir: 'content/assets'
|
||||
},
|
||||
build: {
|
||||
astro_project: 'frontend/site',
|
||||
output_dir: 'dist/site'
|
||||
}
|
||||
};
|
||||
|
||||
function loadSiteConfig(): SiteConfig {
|
||||
try {
|
||||
const file = readFileSync(resolve(siteRoot, 'config/site.yaml'), 'utf8');
|
||||
const parsed = YAML.parse(file) ?? {};
|
||||
|
||||
return {
|
||||
site: { ...defaults.site, ...parsed.site },
|
||||
content: { ...defaults.content, ...parsed.content },
|
||||
build: { ...defaults.build, ...parsed.build }
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
export const siteConfig = loadSiteConfig();
|
||||
export const site = siteConfig.site;
|
||||
64
frontend/site/src/pages/archive/index.astro
Normal file
64
frontend/site/src/pages/archive/index.astro
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
import '../../styles/global.css';
|
||||
import SiteNav from '../../components/SiteNav.astro';
|
||||
import { site } from '../../lib/siteConfig';
|
||||
import { getArchiveYears, tagSlug } from '../../lib/posts';
|
||||
|
||||
const archiveYears = getArchiveYears();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={site.language}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Archive | {site.title}</title>
|
||||
<meta name="description" content={`Archive of ${site.title}`} />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<SiteNav />
|
||||
|
||||
<header class="site-header">
|
||||
<p class="eyebrow">{site.title}</p>
|
||||
<h1>Archive</h1>
|
||||
</header>
|
||||
|
||||
{archiveYears.length === 0 ? (
|
||||
<p class="empty">No published posts yet.</p>
|
||||
) : (
|
||||
<div class="archive-list">
|
||||
{archiveYears.map((group) => (
|
||||
<section class="archive-year" aria-label={group.year}>
|
||||
<h2>{group.year}</h2>
|
||||
<ol>
|
||||
{group.posts.map((post) => (
|
||||
<li>
|
||||
{post.date && (
|
||||
<time datetime={post.date}>
|
||||
{new Date(post.date).toLocaleDateString(site.language, {
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
<a href={post.url}>{post.title}</a>
|
||||
{post.tags.length > 0 && (
|
||||
<ul class="tags compact-tags" aria-label="Tags">
|
||||
{post.tags.map((tag) => (
|
||||
<li>
|
||||
<a href={`/tags/${tagSlug(tag)}/`}>{tag}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
59
frontend/site/src/pages/index.astro
Normal file
59
frontend/site/src/pages/index.astro
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import '../styles/global.css';
|
||||
import SiteNav from '../components/SiteNav.astro';
|
||||
import { site } from '../lib/siteConfig';
|
||||
import { getPublishedPosts, tagSlug } from '../lib/posts';
|
||||
|
||||
const posts = getPublishedPosts();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={site.language}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{site.title}</title>
|
||||
<meta name="description" content={site.description} />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<SiteNav />
|
||||
|
||||
<header class="site-header">
|
||||
<p class="eyebrow">{site.description}</p>
|
||||
<h1>{site.title}</h1>
|
||||
</header>
|
||||
|
||||
<section class="post-list" aria-label="Posts">
|
||||
{posts.length === 0 ? (
|
||||
<p class="empty">No published posts yet.</p>
|
||||
) : (
|
||||
posts.map((post) => (
|
||||
<article class="post-item">
|
||||
<a href={post.url}>
|
||||
<h2>{post.title}</h2>
|
||||
{post.summary && <p>{post.summary}</p>}
|
||||
<div class="post-meta">
|
||||
{post.date && (
|
||||
<time datetime={post.date}>
|
||||
{new Date(post.date).toLocaleDateString(site.language)}
|
||||
</time>
|
||||
)}
|
||||
{post.tags.length > 0 && (
|
||||
<ul class="tags" aria-label="Tags">
|
||||
{post.tags.map((tag) => (
|
||||
<li>
|
||||
<a href={`/tags/${tagSlug(tag)}/`}>{tag}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
56
frontend/site/src/pages/posts/[slug].astro
Normal file
56
frontend/site/src/pages/posts/[slug].astro
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
import '../../styles/global.css';
|
||||
import SiteNav from '../../components/SiteNav.astro';
|
||||
import { site } from '../../lib/siteConfig';
|
||||
import { tagSlug } from '../../lib/posts';
|
||||
|
||||
export function getStaticPaths() {
|
||||
const modules = import.meta.glob('../../../../../content/posts/*.md', { eager: true });
|
||||
return Object.values(modules)
|
||||
.filter((post) => post.frontmatter.status === 'published')
|
||||
.map((post) => ({
|
||||
params: { slug: post.frontmatter.slug },
|
||||
props: { post }
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = post;
|
||||
const title = post.frontmatter.title;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={site.language}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{title} | {site.title}</title>
|
||||
{post.frontmatter.summary && <meta name="description" content={post.frontmatter.summary} />}
|
||||
</head>
|
||||
<body>
|
||||
<main class="page article-page">
|
||||
<SiteNav />
|
||||
|
||||
<article class="article">
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<time datetime={post.frontmatter.published_at ?? post.frontmatter.updated_at}>
|
||||
{new Date(post.frontmatter.published_at ?? post.frontmatter.updated_at).toLocaleDateString(site.language)}
|
||||
</time>
|
||||
{post.frontmatter.summary && <p class="summary">{post.frontmatter.summary}</p>}
|
||||
{post.frontmatter.tags?.length > 0 && (
|
||||
<ul class="tags article-tags" aria-label="Tags">
|
||||
{post.frontmatter.tags.map((tag) => (
|
||||
<li>
|
||||
<a href={`/tags/${tagSlug(tag)}/`}>{tag}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<Content />
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
18
frontend/site/src/pages/rss.xml.ts
Normal file
18
frontend/site/src/pages/rss.xml.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import rss from '@astrojs/rss';
|
||||
import { getPublishedPosts } from '../lib/posts';
|
||||
import { site } from '../lib/siteConfig';
|
||||
|
||||
export function GET() {
|
||||
return rss({
|
||||
title: site.title,
|
||||
description: site.description,
|
||||
site: site.base_url,
|
||||
items: getPublishedPosts().map((post) => ({
|
||||
title: post.title,
|
||||
description: post.summary ?? '',
|
||||
link: post.url,
|
||||
pubDate: post.date ? new Date(post.date) : undefined,
|
||||
categories: post.tags
|
||||
}))
|
||||
});
|
||||
}
|
||||
52
frontend/site/src/pages/tags/[tag].astro
Normal file
52
frontend/site/src/pages/tags/[tag].astro
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
import '../../styles/global.css';
|
||||
import SiteNav from '../../components/SiteNav.astro';
|
||||
import { site } from '../../lib/siteConfig';
|
||||
import { getPostsByTag, getTagSummaries } from '../../lib/posts';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return getTagSummaries().map((tag) => ({
|
||||
params: { tag: tag.slug },
|
||||
props: { tag }
|
||||
}));
|
||||
}
|
||||
|
||||
const { tag } = Astro.props;
|
||||
const posts = getPostsByTag(tag.slug);
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={site.language}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{tag.name} | {site.title}</title>
|
||||
<meta name="description" content={`Posts tagged ${tag.name}`} />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<SiteNav />
|
||||
|
||||
<header class="site-header">
|
||||
<p class="eyebrow">{site.title}</p>
|
||||
<h1>{tag.name}</h1>
|
||||
</header>
|
||||
|
||||
<section class="post-list" aria-label={`Posts tagged ${tag.name}`}>
|
||||
{posts.map((post) => (
|
||||
<article class="post-item">
|
||||
<a href={post.url}>
|
||||
<h2>{post.title}</h2>
|
||||
{post.summary && <p>{post.summary}</p>}
|
||||
{post.date && (
|
||||
<time datetime={post.date}>
|
||||
{new Date(post.date).toLocaleDateString(site.language)}
|
||||
</time>
|
||||
)}
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
43
frontend/site/src/pages/tags/index.astro
Normal file
43
frontend/site/src/pages/tags/index.astro
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
import '../../styles/global.css';
|
||||
import SiteNav from '../../components/SiteNav.astro';
|
||||
import { site } from '../../lib/siteConfig';
|
||||
import { getTagSummaries } from '../../lib/posts';
|
||||
|
||||
const tags = getTagSummaries();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={site.language}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Tags | {site.title}</title>
|
||||
<meta name="description" content={`Tags on ${site.title}`} />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<SiteNav />
|
||||
|
||||
<header class="site-header">
|
||||
<p class="eyebrow">{site.title}</p>
|
||||
<h1>Tags</h1>
|
||||
</header>
|
||||
|
||||
{tags.length === 0 ? (
|
||||
<p class="empty">No tags yet.</p>
|
||||
) : (
|
||||
<ul class="tag-index" aria-label="Tags">
|
||||
{tags.map((tag) => (
|
||||
<li>
|
||||
<a href={`/tags/${tag.slug}/`}>
|
||||
<span>{tag.name}</span>
|
||||
<strong>{tag.count}</strong>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
240
frontend/site/src/styles/global.css
Normal file
240
frontend/site/src/styles/global.css
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
:root {
|
||||
color-scheme: light;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
background: #f7f5ef;
|
||||
color: #20201d;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: min(860px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 56px 0 80px;
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 18px;
|
||||
margin-bottom: 36px;
|
||||
color: #625a50;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.site-nav a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
border-bottom: 1px solid #d8d1c3;
|
||||
padding-bottom: 28px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: #6f675b;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2.4rem, 8vw, 5rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.post-list {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
border-bottom: 1px solid #ded8cd;
|
||||
padding: 18px 0 22px;
|
||||
}
|
||||
|
||||
.post-item a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-item h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.post-item p,
|
||||
.summary,
|
||||
.empty {
|
||||
color: #5b554d;
|
||||
}
|
||||
|
||||
.post-item p {
|
||||
margin: 0 0 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
time {
|
||||
color: #7a7268;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tags li {
|
||||
border: 1px solid #d0c8bb;
|
||||
border-radius: 999px;
|
||||
color: #5f584d;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.tags a {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag-index {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tag-index a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid #ded8cd;
|
||||
padding: 14px 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag-index strong {
|
||||
color: #766f65;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-nav {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.back-nav a {
|
||||
color: #635d53;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.article {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.article header {
|
||||
border-bottom: 1px solid #d8d1c3;
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.article-tags {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.archive-list {
|
||||
display: grid;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.archive-year h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.archive-year ol {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.archive-year li {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ded8cd;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.archive-year li > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.compact-tags {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.archive-year li {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.compact-tags {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.article h1 {
|
||||
margin-bottom: 16px;
|
||||
font-size: clamp(2.1rem, 7vw, 4rem);
|
||||
}
|
||||
|
||||
.article h2 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.article pre {
|
||||
overflow-x: auto;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #272822;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.article code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue