@anglefeint/astro-theme 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/package.json +38 -0
- package/public/scripts/about-effects.js +535 -0
- package/public/scripts/blogpost-effects.js +956 -0
- package/public/scripts/home-matrix.js +117 -0
- package/public/styles/about-page.css +756 -0
- package/public/styles/blog-post.css +390 -0
- package/public/styles/home-page.css +186 -0
- package/src/assets/theme/placeholders/theme-placeholder-1.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-2.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-3.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-4.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-5.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-about.jpg +0 -0
- package/src/assets/theme/red-queen/theme-redqueen1.webp +0 -0
- package/src/assets/theme/red-queen/theme-redqueen2.gif +0 -0
- package/src/cli-new-page.mjs +118 -0
- package/src/cli-new-post.mjs +139 -0
- package/src/components/BaseHead.astro +177 -0
- package/src/components/Footer.astro +74 -0
- package/src/components/FormattedDate.astro +17 -0
- package/src/components/Header.astro +328 -0
- package/src/components/HeaderLink.astro +35 -0
- package/src/consts.ts +12 -0
- package/src/content-schema.ts +33 -0
- package/src/i18n/config.ts +46 -0
- package/src/i18n/messages.ts +220 -0
- package/src/i18n/posts.ts +11 -0
- package/src/index.ts +5 -0
- package/src/layouts/BasePageLayout.astro +67 -0
- package/src/layouts/BlogPost.astro +279 -0
- package/src/layouts/HomePage.astro +73 -0
- package/src/styles/global.css +1867 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { mkdir, writeFile, access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SUPPORTED_LOCALES = ['en', 'ja', 'ko', 'es', 'zh'];
|
|
6
|
+
const CONTENT_ROOT = path.resolve(process.cwd(), 'src/content/blog');
|
|
7
|
+
const DEFAULT_COVERS_ROOT = path.resolve(process.cwd(), 'src/assets/blog/default-covers');
|
|
8
|
+
|
|
9
|
+
function toTitleFromSlug(slug) {
|
|
10
|
+
return slug
|
|
11
|
+
.split('-')
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
|
|
14
|
+
.join(' ');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateSlug(slug) {
|
|
18
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hashString(input) {
|
|
22
|
+
let hash = 5381;
|
|
23
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
24
|
+
hash = ((hash << 5) + hash + input.charCodeAt(i)) >>> 0;
|
|
25
|
+
}
|
|
26
|
+
return hash >>> 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePathForFrontmatter(filePath) {
|
|
30
|
+
return filePath.split(path.sep).join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loadDefaultCovers() {
|
|
34
|
+
try {
|
|
35
|
+
const entries = await readdir(DEFAULT_COVERS_ROOT, { withFileTypes: true });
|
|
36
|
+
return entries
|
|
37
|
+
.filter((entry) => entry.isFile() && /\.(webp|png|jpe?g)$/i.test(entry.name))
|
|
38
|
+
.map((entry) => path.join(DEFAULT_COVERS_ROOT, entry.name))
|
|
39
|
+
.sort((a, b) => a.localeCompare(b));
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function exists(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
await access(filePath, constants.F_OK);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function templateFor(locale, slug, pubDate, heroImage) {
|
|
55
|
+
const titleByLocale = {
|
|
56
|
+
en: toTitleFromSlug(slug),
|
|
57
|
+
ja: '新しい記事タイトル',
|
|
58
|
+
ko: '새 글 제목',
|
|
59
|
+
es: 'Titulo del nuevo articulo',
|
|
60
|
+
zh: '新文章标题',
|
|
61
|
+
};
|
|
62
|
+
const descriptionByLocale = {
|
|
63
|
+
en: `A short EN post scaffold for "${slug}".`,
|
|
64
|
+
ja: `「${slug}」用の短い日本語記事テンプレートです。`,
|
|
65
|
+
ko: `"${slug}"용 한국어 글 템플릿입니다.`,
|
|
66
|
+
es: `Plantilla breve en espanol para "${slug}".`,
|
|
67
|
+
zh: `“${slug}”的中文文章模板。`,
|
|
68
|
+
};
|
|
69
|
+
const bodyByLocale = {
|
|
70
|
+
en: `Write your EN content for "${slug}" here.`,
|
|
71
|
+
ja: `ここに「${slug}」の日本語本文を書いてください。`,
|
|
72
|
+
ko: `여기에 "${slug}" 한국어 본문을 작성하세요.`,
|
|
73
|
+
es: `Escribe aqui el contenido en espanol para "${slug}".`,
|
|
74
|
+
zh: `请在这里填写“${slug}”的中文正文。`,
|
|
75
|
+
};
|
|
76
|
+
return `---
|
|
77
|
+
title: '${titleByLocale[locale]}'
|
|
78
|
+
subtitle: ''
|
|
79
|
+
description: '${descriptionByLocale[locale]}'
|
|
80
|
+
pubDate: '${pubDate}'
|
|
81
|
+
${heroImage ? `heroImage: '${heroImage}'` : ''}
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
${bodyByLocale[locale]}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
const slug = process.argv[2];
|
|
90
|
+
if (!slug) {
|
|
91
|
+
console.error('Usage: npm run new-post -- <slug>');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!validateSlug(slug)) {
|
|
96
|
+
console.error('Invalid slug. Use lowercase letters, numbers, and hyphens only.');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pubDate = new Date().toISOString().slice(0, 10);
|
|
101
|
+
const defaultCovers = await loadDefaultCovers();
|
|
102
|
+
const created = [];
|
|
103
|
+
const skipped = [];
|
|
104
|
+
|
|
105
|
+
for (const locale of SUPPORTED_LOCALES) {
|
|
106
|
+
const localeDir = path.join(CONTENT_ROOT, locale);
|
|
107
|
+
const filePath = path.join(localeDir, `${slug}.md`);
|
|
108
|
+
await mkdir(localeDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
if (await exists(filePath)) {
|
|
111
|
+
skipped.push(filePath);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let heroImage = '';
|
|
116
|
+
if (defaultCovers.length > 0) {
|
|
117
|
+
const coverPath = defaultCovers[hashString(slug) % defaultCovers.length];
|
|
118
|
+
heroImage = normalizePathForFrontmatter(path.relative(localeDir, coverPath));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await writeFile(filePath, templateFor(locale, slug, pubDate, heroImage), 'utf8');
|
|
122
|
+
created.push(filePath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (created.length > 0) {
|
|
126
|
+
console.log('Created files:');
|
|
127
|
+
for (const filePath of created) console.log(`- ${filePath}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (skipped.length > 0) {
|
|
131
|
+
console.log('Skipped existing files:');
|
|
132
|
+
for (const filePath of skipped) console.log(`- ${filePath}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main().catch((error) => {
|
|
137
|
+
console.error(error);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Import the global.css file here so that it is included on
|
|
3
|
+
// all pages through the use of the <BaseHead /> component.
|
|
4
|
+
import '../styles/global.css';
|
|
5
|
+
import type { ImageMetadata } from 'astro';
|
|
6
|
+
import FallbackImage from '../assets/theme/placeholders/theme-placeholder-1.jpg';
|
|
7
|
+
import { SITE_AUTHOR, SITE_TITLE, SITE_URL } from '../consts';
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_LOCALE,
|
|
10
|
+
SUPPORTED_LOCALES,
|
|
11
|
+
alternatePathForLocale,
|
|
12
|
+
isLocale,
|
|
13
|
+
stripLocaleFromPath,
|
|
14
|
+
} from '../i18n/config';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
image?: ImageMetadata;
|
|
20
|
+
pageType?: 'website' | 'article';
|
|
21
|
+
publishedTime?: Date;
|
|
22
|
+
modifiedTime?: Date;
|
|
23
|
+
author?: string;
|
|
24
|
+
tags?: string[];
|
|
25
|
+
schema?: Record<string, unknown> | Record<string, unknown>[];
|
|
26
|
+
noindex?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const siteURL = Astro.site ?? new URL(SITE_URL);
|
|
30
|
+
const canonicalURL = new URL(Astro.url.pathname, siteURL);
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
title,
|
|
34
|
+
description,
|
|
35
|
+
image = FallbackImage,
|
|
36
|
+
pageType = 'website',
|
|
37
|
+
publishedTime,
|
|
38
|
+
modifiedTime,
|
|
39
|
+
author = SITE_AUTHOR,
|
|
40
|
+
tags,
|
|
41
|
+
schema,
|
|
42
|
+
noindex = false,
|
|
43
|
+
} = Astro.props;
|
|
44
|
+
|
|
45
|
+
const imageURL = new URL(image.src, siteURL).toString();
|
|
46
|
+
const robotsContent = noindex ? 'noindex, nofollow' : 'index, follow, max-image-preview:large';
|
|
47
|
+
const isArticle = pageType === 'article';
|
|
48
|
+
const pathParts = Astro.url.pathname.split('/').filter(Boolean);
|
|
49
|
+
const currentLocale = pathParts.length > 0 && isLocale(pathParts[0]) ? pathParts[0] : DEFAULT_LOCALE;
|
|
50
|
+
const rssURL = new URL(`/${currentLocale}/rss.xml`, siteURL);
|
|
51
|
+
const localeSubpath = stripLocaleFromPath(Astro.url.pathname, currentLocale);
|
|
52
|
+
const alternatePaths = SUPPORTED_LOCALES.map((locale) => ({
|
|
53
|
+
locale,
|
|
54
|
+
href: new URL(alternatePathForLocale(locale, localeSubpath), siteURL).toString(),
|
|
55
|
+
}));
|
|
56
|
+
const xDefaultHref = new URL(alternatePathForLocale(DEFAULT_LOCALE, localeSubpath), siteURL);
|
|
57
|
+
|
|
58
|
+
const OG_LOCALE: Record<string, string> = {
|
|
59
|
+
en: 'en_US',
|
|
60
|
+
ja: 'ja_JP',
|
|
61
|
+
ko: 'ko_KR',
|
|
62
|
+
es: 'es_ES',
|
|
63
|
+
zh: 'zh_CN',
|
|
64
|
+
};
|
|
65
|
+
const ogLocale = OG_LOCALE[currentLocale] ?? 'en_US';
|
|
66
|
+
const ogLocaleAlternates = SUPPORTED_LOCALES.filter((l) => l !== currentLocale).map((l) => OG_LOCALE[l]);
|
|
67
|
+
|
|
68
|
+
const websiteSchema = {
|
|
69
|
+
'@context': 'https://schema.org',
|
|
70
|
+
'@type': 'WebSite',
|
|
71
|
+
name: SITE_TITLE,
|
|
72
|
+
url: siteURL.toString(),
|
|
73
|
+
description,
|
|
74
|
+
publisher: {
|
|
75
|
+
'@type': 'Person',
|
|
76
|
+
name: SITE_AUTHOR,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const personSchema = {
|
|
81
|
+
'@context': 'https://schema.org',
|
|
82
|
+
'@type': 'Person',
|
|
83
|
+
name: SITE_AUTHOR,
|
|
84
|
+
url: siteURL.toString(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const articleSchema = isArticle
|
|
88
|
+
? {
|
|
89
|
+
'@context': 'https://schema.org',
|
|
90
|
+
'@type': 'BlogPosting',
|
|
91
|
+
headline: title,
|
|
92
|
+
description,
|
|
93
|
+
image: [imageURL],
|
|
94
|
+
author: {
|
|
95
|
+
'@type': 'Person',
|
|
96
|
+
name: author,
|
|
97
|
+
},
|
|
98
|
+
publisher: {
|
|
99
|
+
'@type': 'Person',
|
|
100
|
+
name: SITE_AUTHOR,
|
|
101
|
+
},
|
|
102
|
+
mainEntityOfPage: canonicalURL.toString(),
|
|
103
|
+
datePublished: publishedTime?.toISOString(),
|
|
104
|
+
dateModified: (modifiedTime ?? publishedTime)?.toISOString(),
|
|
105
|
+
keywords: tags?.length ? tags.join(', ') : undefined,
|
|
106
|
+
}
|
|
107
|
+
: undefined;
|
|
108
|
+
|
|
109
|
+
const extraSchemas = schema ? (Array.isArray(schema) ? schema : [schema]) : [];
|
|
110
|
+
const jsonLdSchemas = [websiteSchema, personSchema, articleSchema, ...extraSchemas].filter(Boolean);
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
<!-- Global Metadata -->
|
|
114
|
+
<meta charset="utf-8" />
|
|
115
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
116
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
117
|
+
<link rel="icon" href="/favicon.ico" />
|
|
118
|
+
<link rel="sitemap" href="/sitemap-index.xml" />
|
|
119
|
+
<link
|
|
120
|
+
rel="alternate"
|
|
121
|
+
type="application/rss+xml"
|
|
122
|
+
title={SITE_TITLE}
|
|
123
|
+
href={rssURL}
|
|
124
|
+
/>
|
|
125
|
+
<meta name="generator" content={Astro.generator} />
|
|
126
|
+
<meta name="robots" content={robotsContent} />
|
|
127
|
+
<meta name="author" content={author} />
|
|
128
|
+
|
|
129
|
+
<!-- Font preloads -->
|
|
130
|
+
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
|
|
131
|
+
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
|
|
132
|
+
|
|
133
|
+
<!-- Canonical URL -->
|
|
134
|
+
<link rel="canonical" href={canonicalURL} />
|
|
135
|
+
{
|
|
136
|
+
alternatePaths.map((alternate) => (
|
|
137
|
+
<link rel="alternate" hreflang={alternate.locale} href={alternate.href} />
|
|
138
|
+
))
|
|
139
|
+
}
|
|
140
|
+
<link rel="alternate" hreflang="x-default" href={xDefaultHref} />
|
|
141
|
+
|
|
142
|
+
<!-- Primary Meta Tags -->
|
|
143
|
+
<title>{title}</title>
|
|
144
|
+
<meta name="title" content={title} />
|
|
145
|
+
<meta name="description" content={description} />
|
|
146
|
+
{tags && tags.length > 0 && <meta name="keywords" content={tags.join(', ')} />}
|
|
147
|
+
|
|
148
|
+
<!-- Open Graph / Facebook -->
|
|
149
|
+
<meta property="og:type" content={isArticle ? 'article' : 'website'} />
|
|
150
|
+
<meta property="og:url" content={canonicalURL} />
|
|
151
|
+
<meta property="og:title" content={title} />
|
|
152
|
+
<meta property="og:description" content={description} />
|
|
153
|
+
<meta property="og:image" content={imageURL} />
|
|
154
|
+
{typeof image?.width === 'number' && <meta property="og:image:width" content={String(image.width)} />}
|
|
155
|
+
{typeof image?.height === 'number' && <meta property="og:image:height" content={String(image.height)} />}
|
|
156
|
+
<meta property="og:locale" content={ogLocale} />
|
|
157
|
+
{ogLocaleAlternates.map((loc) => (
|
|
158
|
+
<meta property="og:locale:alternate" content={loc} />
|
|
159
|
+
))}
|
|
160
|
+
{isArticle && publishedTime && <meta property="article:published_time" content={publishedTime.toISOString()} />}
|
|
161
|
+
{isArticle && (
|
|
162
|
+
<meta property="article:modified_time" content={(modifiedTime ?? publishedTime)?.toISOString()} />
|
|
163
|
+
)}
|
|
164
|
+
{isArticle && <meta property="article:author" content={author} />}
|
|
165
|
+
|
|
166
|
+
<!-- Twitter -->
|
|
167
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
168
|
+
<meta name="twitter:url" content={canonicalURL} />
|
|
169
|
+
<meta name="twitter:title" content={title} />
|
|
170
|
+
<meta name="twitter:description" content={description} />
|
|
171
|
+
<meta name="twitter:image" content={imageURL} />
|
|
172
|
+
|
|
173
|
+
{
|
|
174
|
+
jsonLdSchemas.map((schemaItem) => (
|
|
175
|
+
<script is:inline type="application/ld+json" set:html={JSON.stringify(schemaItem)} />
|
|
176
|
+
))
|
|
177
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SITE_TAGLINE, SITE_TITLE } from '@anglefeint/site-config/site';
|
|
3
|
+
import { SOCIAL_LINKS } from '@anglefeint/site-config/social';
|
|
4
|
+
|
|
5
|
+
const today = new Date();
|
|
6
|
+
interface Props {
|
|
7
|
+
tagline?: string;
|
|
8
|
+
/** Show Blade Runner scanlines overlay on mesh-page (header/footer only) */
|
|
9
|
+
scanlines?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { tagline = SITE_TAGLINE, scanlines = false } = Astro.props as Props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<footer>
|
|
16
|
+
© {today.getFullYear()} {SITE_TITLE}. {tagline}
|
|
17
|
+
{SOCIAL_LINKS.length > 0 && (
|
|
18
|
+
<div class="social-links">
|
|
19
|
+
{SOCIAL_LINKS.map((link) => (
|
|
20
|
+
<a href={link.href} target="_blank" rel="noopener noreferrer">
|
|
21
|
+
<span class="sr-only">{link.label}</span>
|
|
22
|
+
{link.icon === 'mastodon' && (
|
|
23
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
|
|
24
|
+
<path
|
|
25
|
+
fill="currentColor"
|
|
26
|
+
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
29
|
+
)}
|
|
30
|
+
{link.icon === 'twitter' && (
|
|
31
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
|
|
32
|
+
<path
|
|
33
|
+
fill="currentColor"
|
|
34
|
+
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
)}
|
|
38
|
+
{link.icon === 'github' && (
|
|
39
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
|
|
40
|
+
<path
|
|
41
|
+
fill="currentColor"
|
|
42
|
+
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
)}
|
|
46
|
+
{!link.icon && <span>{link.label}</span>}
|
|
47
|
+
</a>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
{scanlines && <div class="mesh-scanlines" aria-hidden="true"></div>}
|
|
52
|
+
</footer>
|
|
53
|
+
<style>
|
|
54
|
+
footer {
|
|
55
|
+
padding: 2em 1em 3em 1em;
|
|
56
|
+
background: var(--chrome-bg, var(--bg));
|
|
57
|
+
border-top: 1px solid var(--chrome-border, rgb(var(--border)));
|
|
58
|
+
color: var(--chrome-text-muted, rgb(var(--text-muted)));
|
|
59
|
+
text-align: center;
|
|
60
|
+
}
|
|
61
|
+
.social-links {
|
|
62
|
+
display: flex;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
gap: 1em;
|
|
65
|
+
margin-top: 1em;
|
|
66
|
+
}
|
|
67
|
+
.social-links a {
|
|
68
|
+
text-decoration: none;
|
|
69
|
+
color: var(--chrome-text-muted, rgb(var(--text-muted)));
|
|
70
|
+
}
|
|
71
|
+
.social-links a:hover {
|
|
72
|
+
color: var(--chrome-link-hover, var(--chrome-link, rgb(var(--text))));
|
|
73
|
+
}
|
|
74
|
+
</style>
|