aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-Bold.ttfbin0 -> 277828 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-BoldItalic.ttfbin0 -> 279832 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-ExtraBold.ttfbin0 -> 279404 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-ExtraBoldItalic.ttfbin0 -> 281616 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-ExtraLight.ttfbin0 -> 274144 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-ExtraLightItalic.ttfbin0 -> 274240 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-Italic.ttfbin0 -> 276840 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-Light.ttfbin0 -> 276452 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-LightItalic.ttfbin0 -> 277104 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-Medium.ttfbin0 -> 273860 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-MediumItalic.ttfbin0 -> 276804 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-Regular.ttfbin0 -> 273900 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-SemiBold.ttfbin0 -> 277092 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-SemiBoldItalic.ttfbin0 -> 279828 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-Thin.ttfbin0 -> 270112 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMono-ThinItalic.ttfbin0 -> 272984 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-Bold.ttfbin0 -> 210988 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-BoldItalic.ttfbin0 -> 214132 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBold.ttfbin0 -> 213372 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBoldItalic.ttfbin0 -> 215456 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLight.ttfbin0 -> 209072 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLightItalic.ttfbin0 -> 209884 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-Italic.ttfbin0 -> 211624 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-Light.ttfbin0 -> 210840 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-LightItalic.ttfbin0 -> 212320 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-Medium.ttfbin0 -> 208276 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-MediumItalic.ttfbin0 -> 211604 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-Regular.ttfbin0 -> 208576 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBold.ttfbin0 -> 209864 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBoldItalic.ttfbin0 -> 214032 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-Thin.ttfbin0 -> 206004 bytes
-rw-r--r--src/assets/JetBrainsMono/JetBrainsMonoNL-ThinItalic.ttfbin0 -> 209124 bytes
-rw-r--r--src/components/Analytics.astro18
-rw-r--r--src/components/Comments.astro1
-rw-r--r--src/components/Head.astro33
-rw-r--r--src/components/Header.astro16
-rw-r--r--src/components/Icons/Email.astro20
-rw-r--r--src/components/Icons/GitHub.astro22
-rw-r--r--src/components/Icons/LinkedIn.astro21
-rw-r--r--src/components/Icons/RSS.astro17
-rw-r--r--src/components/JsonLd.astro13
-rw-r--r--src/components/Pagination.astro35
-rw-r--r--src/components/PostElement.astro40
-rw-r--r--src/components/PostSummary.astro49
-rw-r--r--src/components/Sections/LatestPosts.astro43
-rw-r--r--src/components/Sections/SocialLinks.astro19
-rw-r--r--src/components/Sections/Welcome.astro7
-rw-r--r--src/config.ts29
-rw-r--r--src/content/blog/create-lib-file-from-dll.md10
-rw-r--r--src/content/blog/electron-reload.md7
-rw-r--r--src/content/blog/example-content.md5
-rw-r--r--src/content/blog/getting-source-code-of-chromium.md7
-rw-r--r--src/content/blog/installing-moodle-to-fedora.md7
-rw-r--r--src/content/blog/rust-and-tl-mr3020.md7
-rw-r--r--src/content/config.ts6
-rw-r--r--src/integrations/ogImages.ts47
-rw-r--r--src/layouts/BaseLayout.astro24
-rw-r--r--src/pages/404.astro23
-rw-r--r--src/pages/[...page].astro33
-rw-r--r--src/pages/blog/[...slug].astro45
-rw-r--r--src/pages/blog/index.astro61
-rw-r--r--src/pages/feed.xml.js9
-rw-r--r--src/pages/index.astro27
-rw-r--r--src/scss/_framework.scss2
-rw-r--r--src/scss/_variables.scss2
-rw-r--r--src/scss/global.scss6
-rw-r--r--src/utils/createOgImage.ts52
-rw-r--r--src/utils/ogResources.ts15
-rw-r--r--src/utils/schemas/blogPostSchema.ts37
-rw-r--r--src/utils/schemas/blogSchema.ts26
-rw-r--r--src/utils/schemas/pageSchema.ts23
71 files changed, 672 insertions, 192 deletions
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-Bold.ttf b/src/assets/JetBrainsMono/JetBrainsMono-Bold.ttf
new file mode 100644
index 0000000..8c93043
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-Bold.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-BoldItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-BoldItalic.ttf
new file mode 100644
index 0000000..1ddf216
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-BoldItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-ExtraBold.ttf b/src/assets/JetBrainsMono/JetBrainsMono-ExtraBold.ttf
new file mode 100644
index 0000000..435d7a7
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-ExtraBold.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-ExtraBoldItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-ExtraBoldItalic.ttf
new file mode 100644
index 0000000..79e616e
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-ExtraBoldItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-ExtraLight.ttf b/src/assets/JetBrainsMono/JetBrainsMono-ExtraLight.ttf
new file mode 100644
index 0000000..c131cbf
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-ExtraLight.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-ExtraLightItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-ExtraLightItalic.ttf
new file mode 100644
index 0000000..a768985
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-ExtraLightItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-Italic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-Italic.ttf
new file mode 100644
index 0000000..ccc9d6a
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-Italic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-Light.ttf b/src/assets/JetBrainsMono/JetBrainsMono-Light.ttf
new file mode 100644
index 0000000..15f15a2
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-Light.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-LightItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-LightItalic.ttf
new file mode 100644
index 0000000..506208f
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-LightItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-Medium.ttf b/src/assets/JetBrainsMono/JetBrainsMono-Medium.ttf
new file mode 100644
index 0000000..9767115
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-Medium.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-MediumItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-MediumItalic.ttf
new file mode 100644
index 0000000..415a9e3
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-MediumItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-Regular.ttf b/src/assets/JetBrainsMono/JetBrainsMono-Regular.ttf
new file mode 100644
index 0000000..dff66cc
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-Regular.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-SemiBold.ttf b/src/assets/JetBrainsMono/JetBrainsMono-SemiBold.ttf
new file mode 100644
index 0000000..a70e69b
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-SemiBold.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-SemiBoldItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-SemiBoldItalic.ttf
new file mode 100644
index 0000000..968602e
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-SemiBoldItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-Thin.ttf b/src/assets/JetBrainsMono/JetBrainsMono-Thin.ttf
new file mode 100644
index 0000000..7dbe2ac
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-Thin.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMono-ThinItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMono-ThinItalic.ttf
new file mode 100644
index 0000000..c6ad6c2
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMono-ThinItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-Bold.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-Bold.ttf
new file mode 100644
index 0000000..f78f84f
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-Bold.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-BoldItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-BoldItalic.ttf
new file mode 100644
index 0000000..9fb8c83
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-BoldItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBold.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBold.ttf
new file mode 100644
index 0000000..fe5be6a
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBold.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBoldItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBoldItalic.ttf
new file mode 100644
index 0000000..59fc980
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraBoldItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLight.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLight.ttf
new file mode 100644
index 0000000..6da7b75
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLight.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLightItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLightItalic.ttf
new file mode 100644
index 0000000..5733efc
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-ExtraLightItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-Italic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-Italic.ttf
new file mode 100644
index 0000000..4e9c380
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-Italic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-Light.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-Light.ttf
new file mode 100644
index 0000000..0b79b0c
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-Light.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-LightItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-LightItalic.ttf
new file mode 100644
index 0000000..b5e0842
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-LightItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-Medium.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-Medium.ttf
new file mode 100644
index 0000000..1454372
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-Medium.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-MediumItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-MediumItalic.ttf
new file mode 100644
index 0000000..8d63c6c
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-MediumItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-Regular.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-Regular.ttf
new file mode 100644
index 0000000..70d2ec9
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-Regular.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBold.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBold.ttf
new file mode 100644
index 0000000..ce60a88
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBold.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBoldItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBoldItalic.ttf
new file mode 100644
index 0000000..3b3f8f6
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-SemiBoldItalic.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-Thin.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-Thin.ttf
new file mode 100644
index 0000000..bea837e
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-Thin.ttf
Binary files differ
diff --git a/src/assets/JetBrainsMono/JetBrainsMonoNL-ThinItalic.ttf b/src/assets/JetBrainsMono/JetBrainsMonoNL-ThinItalic.ttf
new file mode 100644
index 0000000..f0bfed7
--- /dev/null
+++ b/src/assets/JetBrainsMono/JetBrainsMonoNL-ThinItalic.ttf
Binary files differ
diff --git a/src/components/Analytics.astro b/src/components/Analytics.astro
index 428ad4f..0c22f52 100644
--- a/src/components/Analytics.astro
+++ b/src/components/Analytics.astro
@@ -1,17 +1 @@
----
-type Props = {
- readonly title: string;
-};
-
-const path = Astro.url.pathname;
-const { title } = Astro.props;
----
-
-<!-- AppMetrix -->
-<script is:inline src="https://appmetrix.com/pixel/T5X0z12SoASBV8Dv"></script>
-
-<!-- GoatCounter -->
-<script is:inline data-goatcounter="https://analytics.popov.link/count" src="//gc.zgo.at/count.js"></script>
-<noscript>
- <img alt="pixel" src={`https://analytics.popov.link/count?p=${encodeURI(path)}&t=${encodeURI(title)}`} />
-</noscript>
+<script is:inline defer src="https://appmetrix.com/pixel/T5X0z12SoASBV8Dv"></script>
diff --git a/src/components/Comments.astro b/src/components/Comments.astro
index 5474d89..c2d6531 100644
--- a/src/components/Comments.astro
+++ b/src/components/Comments.astro
@@ -15,6 +15,7 @@ const theme = "transparent_dark";
<script
is:inline
+ defer
src="https://giscus.app/client.js"
data-category-id={categoryId}
data-category={category}
diff --git a/src/components/Head.astro b/src/components/Head.astro
index 8ed2224..3fded95 100644
--- a/src/components/Head.astro
+++ b/src/components/Head.astro
@@ -1,14 +1,22 @@
---
+import type { WithContext, Thing } from "schema-dts";
+import JsonLd from "./JsonLd.astro";
+
type Props = {
readonly description: string;
+ readonly preview: string;
+ readonly schema: WithContext<Thing>;
readonly title: string;
};
-const canonicalURL = new URL(Astro.url.pathname, Astro.site);
-const { description, title } = Astro.props;
+const { description, preview, schema, title } = Astro.props;
+
+const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
+const previewUrl = new URL(preview, Astro.site);
---
<head>
+ <!-- Meta Tags -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
@@ -18,12 +26,29 @@ const { description, title } = Astro.props;
<link href="/feed.xml" rel="alternate" title="RSS" type="application/atom+xml" />
<link href="/sitemap-index.xml" rel="sitemap" />
- <link href={canonicalURL} rel="canonical" />
+ <link href={canonicalUrl} rel="canonical" />
<title>{title}</title>
- <link rel="icon" href="/favicon.png" />
+ <!-- Icons -->
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
+ <link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#ffffff" />
+
+ <!-- Open Graph -->
+ <meta property="og:type" content="website" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={description} />
+ <meta property="og:image" content={previewUrl} />
+ <meta property="og:url" content={canonicalUrl} />
+
+ <!-- Twitter Cards -->
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:title" content={title} />
+ <meta name="twitter:description" content={description} />
+ <meta name="twitter:image" content={previewUrl} />
+
+ <JsonLd schema={schema} />
</head>
diff --git a/src/components/Header.astro b/src/components/Header.astro
new file mode 100644
index 0000000..f9a0c57
--- /dev/null
+++ b/src/components/Header.astro
@@ -0,0 +1,16 @@
+<style lang="scss">
+ a {
+ margin-right: 1.5rem;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+</style>
+
+<header>
+ <nav aria-label="Navigation">
+ <a href="/" lang="en" aria-label="Home">Home</a>
+ <a href="/blog/" lang="en" aria-label="Blog">Blog</a>
+ </nav>
+</header>
diff --git a/src/components/Icons/Email.astro b/src/components/Icons/Email.astro
new file mode 100644
index 0000000..42391e9
--- /dev/null
+++ b/src/components/Icons/Email.astro
@@ -0,0 +1,20 @@
+<style lang="scss">
+ @use "../../scss/variables" as *;
+
+ a {
+ color: $colorText;
+ display: inline-block;
+ margin: 0 0.5rem;
+ }
+
+ svg {
+ vertical-align: middle;
+ }
+</style>
+
+<a href="mailto:valentin@popov.link" title="E-Mail" rel="noopener" target="_blank">
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="E-Mail" aria-hidden="true">
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
+ <polyline points="22,6 12,13 2,6"></polyline>
+ </svg>
+</a>
diff --git a/src/components/Icons/GitHub.astro b/src/components/Icons/GitHub.astro
new file mode 100644
index 0000000..845dafc
--- /dev/null
+++ b/src/components/Icons/GitHub.astro
@@ -0,0 +1,22 @@
+<style lang="scss">
+ @use "../../scss/variables" as *;
+
+ a {
+ color: $colorText;
+ display: inline-block;
+ margin: 0 0.5rem;
+ }
+
+ svg {
+ vertical-align: middle;
+ }
+</style>
+
+<a href="https://github.com/valentineus" title="GitHub" rel="noopener" target="_blank">
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="GitHub" aria-hidden="true">
+ <path
+ d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
+ >
+ </path>
+ </svg>
+</a>
diff --git a/src/components/Icons/LinkedIn.astro b/src/components/Icons/LinkedIn.astro
new file mode 100644
index 0000000..bbcf6b4
--- /dev/null
+++ b/src/components/Icons/LinkedIn.astro
@@ -0,0 +1,21 @@
+<style lang="scss">
+ @use "../../scss/variables" as *;
+
+ a {
+ color: $colorText;
+ display: inline-block;
+ margin: 0 0.5rem;
+ }
+
+ svg {
+ vertical-align: middle;
+ }
+</style>
+
+<a href="https://www.linkedin.com/in/valentineus/" title="LinkedIn" rel="noopener" target="_blank">
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="LinkedIn" aria-hidden="true">
+ <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
+ <rect x="2" y="9" width="4" height="12"></rect>
+ <circle cx="4" cy="4" r="2"></circle>
+ </svg>
+</a>
diff --git a/src/components/Icons/RSS.astro b/src/components/Icons/RSS.astro
new file mode 100644
index 0000000..f487fb8
--- /dev/null
+++ b/src/components/Icons/RSS.astro
@@ -0,0 +1,17 @@
+<style lang="scss">
+ a {
+ display: inline-block;
+ }
+
+ svg {
+ vertical-align: middle;
+ }
+</style>
+
+<a href="/feed.xml" title="RSS Feed" rel="noopener" target="_blank">
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="RSS Feed" aria-hidden="true">
+ <path d="M4 11a9 9 0 0 1 9 9"></path>
+ <path d="M4 4a16 16 0 0 1 16 16"></path>
+ <circle cx="5" cy="19" r="1"></circle>
+ </svg>
+</a>
diff --git a/src/components/JsonLd.astro b/src/components/JsonLd.astro
new file mode 100644
index 0000000..b58efd7
--- /dev/null
+++ b/src/components/JsonLd.astro
@@ -0,0 +1,13 @@
+---
+import type { WithContext, Thing } from "schema-dts";
+
+type Props = {
+ readonly schema: WithContext<Thing>;
+};
+
+const { schema } = Astro.props;
+const json = JSON.stringify(schema);
+---
+
+<!-- JSON-LD -->
+<script is:inline type="application/ld+json" set:html={json} />
diff --git a/src/components/Pagination.astro b/src/components/Pagination.astro
deleted file mode 100644
index 0d656df..0000000
--- a/src/components/Pagination.astro
+++ /dev/null
@@ -1,35 +0,0 @@
----
-type Props = {
- readonly nextUrl?: string;
- readonly prevUrl?: string;
-};
-
-const { nextUrl, prevUrl } = Astro.props;
----
-
-<style lang="scss">
- div {
- text-align: center;
- }
-
- span {
- margin: 0 2em;
- }
-</style>
-
-<div>
- {
- prevUrl && (
- <span>
- <a href={prevUrl}>&lt; Prev</a>
- </span>
- )
- }
- {
- nextUrl && (
- <span>
- <a href={nextUrl}>Next &gt;</a>
- </span>
- )
- }
-</div>
diff --git a/src/components/PostElement.astro b/src/components/PostElement.astro
new file mode 100644
index 0000000..8b4b7c4
--- /dev/null
+++ b/src/components/PostElement.astro
@@ -0,0 +1,40 @@
+---
+import { type CollectionEntry } from "astro:content";
+import dayjs from "dayjs";
+
+type Props = {
+ readonly post: CollectionEntry<"blog">;
+};
+
+const { post } = Astro.props;
+const { remarkPluginFrontmatter } = await post.render();
+
+const formattedDate = dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY");
+const datePublished = post.data.datePublished.toISOString();
+---
+
+<style lang="scss">
+ @use "../scss/variables" as *;
+
+ a {
+ color: $colorText;
+ }
+
+ small {
+ font-size: $fontSizeBase * 0.75;
+ opacity: 0.5;
+ }
+</style>
+
+<li>
+ <article>
+ <a href={`/blog/${post.slug}`} lang={post.data.lang}>{post.data.title}</a>
+ <div>
+ <small>
+ <time datetime={datePublished} lang="en">{formattedDate}</time>
+ <span>•</span>
+ <span>{remarkPluginFrontmatter.minutesRead}</span>
+ </small>
+ </div>
+ </article>
+</li>
diff --git a/src/components/PostSummary.astro b/src/components/PostSummary.astro
deleted file mode 100644
index 329f1ce..0000000
--- a/src/components/PostSummary.astro
+++ /dev/null
@@ -1,49 +0,0 @@
----
-import { type CollectionEntry } from "astro:content";
-import dayjs from "dayjs";
-
-type Props = {
- readonly post: CollectionEntry<"blog">;
-};
-
-const { post } = Astro.props;
-const { remarkPluginFrontmatter } = await post.render();
-const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
----
-
-<style lang="scss">
- @import "../scss/_variables.scss";
-
- a {
- color: $colorText;
- display: block;
- padding-bottom: 3rem;
-
- &:visited {
- color: $colorText;
- }
- }
-
- h2 {
- color: $colorBlossom;
- font-size: 1.25em;
- margin: 0.5em 0;
- }
-
- div {
- font-size: $fontSizeBase * 0.75;
- opacity: 0.5;
- }
-</style>
-
-<a href={`/blog/${post.slug}`}>
- <article>
- <div>
- <time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
- <span>•</span>
- <span>{remarkPluginFrontmatter.minutesRead}</span>
- </div>
- <h2>{post.data.title}</h2>
- <p>{post.data.description}</p>
- </article>
-</a>
diff --git a/src/components/Sections/LatestPosts.astro b/src/components/Sections/LatestPosts.astro
new file mode 100644
index 0000000..e514ff5
--- /dev/null
+++ b/src/components/Sections/LatestPosts.astro
@@ -0,0 +1,43 @@
+---
+import { getCollection } from "astro:content";
+import dayjs from "dayjs";
+import RSSIcon from "../Icons/RSS.astro";
+
+const posts = await getCollection("blog", ({ data }) => {
+ return data.draft !== true;
+});
+
+posts.sort((a, b) => b.data.datePublished.getTime() - a.data.datePublished.getTime());
+
+const latestPosts = posts.slice(0, 5);
+---
+
+<style lang="scss">
+ @use "../../scss/variables" as *;
+
+ small {
+ font-size: $fontSizeBase * 0.75;
+ opacity: 0.5;
+ }
+</style>
+
+<section>
+ <h2>Latest posts <RSSIcon /></h2>
+ <ul>
+ {
+ latestPosts.map((post) => (
+ <li>
+ <a href={`/blog/${post.slug}`} lang={post.data.lang}>
+ {post.data.title}
+ </a>
+
+ <small>
+ <time datetime={post.data.datePublished.toISOString()} lang="en">
+ {dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY")}
+ </time>
+ </small>
+ </li>
+ ))
+ }
+ </ul>
+</section>
diff --git a/src/components/Sections/SocialLinks.astro b/src/components/Sections/SocialLinks.astro
new file mode 100644
index 0000000..c804b60
--- /dev/null
+++ b/src/components/Sections/SocialLinks.astro
@@ -0,0 +1,19 @@
+---
+import GitHubIcon from "../Icons/GitHub.astro";
+import LinkedInIcon from "../Icons/LinkedIn.astro";
+import EmailIcon from "../Icons/Email.astro";
+---
+
+<style lang="scss">
+ div {
+ margin-bottom: 2rem;
+ }
+</style>
+
+<section>
+ <div>
+ <GitHubIcon />
+ <LinkedInIcon />
+ <EmailIcon />
+ </div>
+</section>
diff --git a/src/components/Sections/Welcome.astro b/src/components/Sections/Welcome.astro
new file mode 100644
index 0000000..ee7cae5
--- /dev/null
+++ b/src/components/Sections/Welcome.astro
@@ -0,0 +1,7 @@
+<section>
+ <div>
+ <h1>Hi, I'm Valentin 👋</h1>
+ <p>I'm a professional software developer currently working as a project manager and team lead. On my personal website, I share thoughts on tech, leadership, and digital life.</p>
+ <p>Welcome, and feel free to explore!</p>
+ </div>
+</section>
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..d0c98ea
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,29 @@
+export const config = {
+ author: {
+ name: "Valentin Popov",
+ email: "valentin@popov.link",
+ url: "https://popov.link/",
+ sameAs: ["https://www.linkedin.com/in/valentineus/", "https://github.com/valentineus"],
+ },
+
+ // Open Graph
+ og: {
+ color: {
+ bg: "#181818",
+ bgCode: "#3b3d42",
+ blossom: "#6da13f",
+ text: "#dee2e6",
+ },
+ defaultPreview: "/images/photo.png",
+ dimensions: {
+ height: 630,
+ width: 1200,
+ },
+ fonts: {
+ bold: "./src/assets/JetBrainsMono/JetBrainsMono-Bold.ttf",
+ regular: "./src/assets/JetBrainsMono/JetBrainsMono-Regular.ttf",
+ },
+ photo: "./public/images/photo.png",
+ website: "popov.link",
+ },
+};
diff --git a/src/content/blog/create-lib-file-from-dll.md b/src/content/blog/create-lib-file-from-dll.md
index 54b61e2..edece47 100644
--- a/src/content/blog/create-lib-file-from-dll.md
+++ b/src/content/blog/create-lib-file-from-dll.md
@@ -1,8 +1,10 @@
---
-title: 'Create ".lib" file from ".dll" (archive)'
-author: "Adrian Henke"
-pubDate: "2023-05-04"
-description: "Learn how to generate a *.lib file from a *.dll with this comprehensive guide. Using the Visual Studio Command Prompt and Microsoft's recommended tools, this article walks you through the steps for a seamless process. Perfect for developers working with 3rd party win dll's."
+basedOn: "https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/"
+title: "Create .lib file from .dll (archive)"
+description: "Quick guide to create a .lib from a .dll on Windows: list exports with dumpbin, make a .def file, then generate the import library with lib."
+datePublished: "2023-05-04"
+dateModified: "2023-05-04"
+lang: "en"
---
> This's a copy of a non-my post. The original article [is here](https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/) ([archive](https://web.archive.org/web/20161118122539/https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/)).
diff --git a/src/content/blog/electron-reload.md b/src/content/blog/electron-reload.md
index a07d274..a9376ac 100644
--- a/src/content/blog/electron-reload.md
+++ b/src/content/blog/electron-reload.md
@@ -1,8 +1,9 @@
---
title: "Горячая перезагрузка ElectronJS приложения"
-author: "Valentin Popov"
-pubDate: "2019-08-15"
-description: "Руководство по автоматической перезагрузке приложений на Electron с помощью пакетов electron-reload и electron-webpack. Обход проблем с совместимостью и использование HMR для renderer процесса."
+description: "Горячая перезагрузка ElectronJS: перезапуск main через nodemon и автообновление renderer с HMR/chokidar. Пошагово, без electron-reload и с Webpack."
+datePublished: "2019-08-15"
+dateModified: "2019-08-15"
+lang: "ru"
---
## Main процесс
diff --git a/src/content/blog/example-content.md b/src/content/blog/example-content.md
index 662b2b3..28ce23a 100644
--- a/src/content/blog/example-content.md
+++ b/src/content/blog/example-content.md
@@ -1,8 +1,9 @@
---
title: "Example Content"
-author: "Example User"
-pubDate: "2018-01-01"
description: "Howdy! This is an example blog post that shows several types of HTML content supported in this theme."
+datePublished: "2018-01-01"
+dateModified: "2018-01-01"
+lang: "en"
draft: true
---
diff --git a/src/content/blog/getting-source-code-of-chromium.md b/src/content/blog/getting-source-code-of-chromium.md
index 161a40f..5bc5345 100644
--- a/src/content/blog/getting-source-code-of-chromium.md
+++ b/src/content/blog/getting-source-code-of-chromium.md
@@ -1,8 +1,9 @@
---
title: 'Получение исходного кода "Chromium Projects"'
-author: "Valentin Popov"
-pubDate: "2012-01-30"
-description: "Изучение исходных кодов Chromium: подготовка системы и установка необходимых программных компонентов. Руководство для начинающих разработчиков. Получите инструкции по установке Microsoft Visual Studio, Cygwin, Python и других инструментов. Действительно на январь-февраль 2012 года."
+description: "Как получить и подготовить исходники Chromium на Windows: Visual Studio, Cygwin, depot_tools, команды gclient. Краткая пошаговая инструкция."
+datePublished: "2012-01-30"
+dateModified: "2012-01-30"
+lang: "ru"
---
> Перенос [оригинальной статьи](https://adeptus-mechanicus.blogspot.com/2012/01/chromium-projects.html) 2012 года из моего [старого блога](https://adeptus-mechanicus.blogspot.com/) ([зеркало](https://web.archive.org/web/20160217052148/http://adeptus-mechanicus.blogspot.com/)).
diff --git a/src/content/blog/installing-moodle-to-fedora.md b/src/content/blog/installing-moodle-to-fedora.md
index 12e2e4e..a331dec 100644
--- a/src/content/blog/installing-moodle-to-fedora.md
+++ b/src/content/blog/installing-moodle-to-fedora.md
@@ -1,8 +1,9 @@
---
title: "Установка Moodle в Fedora"
-author: "Valentin Popov"
-pubDate: "2018-07-23"
-description: "Решение проблем установки Moodle из-за SELinux: как настроить правила доступа для устранения ошибок в веб-интерфейсе и при работе с cURL. Практические советы и команды."
+description: "Установка Moodle в Fedora: как исправить зависание инсталлятора и cURL error из-за SELinux. Правильные setsebool и chcon для доступа к сети и каталогам."
+datePublished: "2018-07-23"
+dateModified: "2018-07-23"
+lang: "ru"
---
Во время установки Moodle, сталкиваешься со следующими проблемами:
diff --git a/src/content/blog/rust-and-tl-mr3020.md b/src/content/blog/rust-and-tl-mr3020.md
index 2e23f3b..5734b7a 100644
--- a/src/content/blog/rust-and-tl-mr3020.md
+++ b/src/content/blog/rust-and-tl-mr3020.md
@@ -1,8 +1,9 @@
---
title: "Компиляция Rust на TL-MR3020"
-author: "Valentin Popov"
-pubDate: "2023-05-01"
-description: 'Как настроить и оптимизировать проект Rust для кросс-компиляции на TP-Link TL-MR3020 с использованием Fedora Linux 38 и OpenWrt 22.03.4. Шаг за шагом от базового "Hello, World!" до асинхронного TCP сервера.'
+description: "Кросс-компиляция Rust для OpenWrt на TL-MR3020 (MIPS): rustup, cross-rs, Docker/Podman, UPX, пример TCP-сервера и сжатие бинарника."
+datePublished: "2023-05-01"
+dateModified: "2023-05-01"
+lang: "ru"
---
Информация в статье актуальна для дистрибутива [Fedora Linux 38](https://docs.fedoraproject.org/en-US/releases/f38/), прошивки [OpenWrt 22.03.4](https://openwrt.org/releases/22.03/notes-22.03.4) и устройства [TP-Link TL-MR3020](https://www.tp-link.com/en/home-networking/3g-4g-router/tl-mr3020/) ревизии v3.20.
diff --git a/src/content/config.ts b/src/content/config.ts
index 245f20e..4277edc 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -3,10 +3,12 @@ import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
- author: z.string(),
+ basedOn: z.optional(z.string()),
+ dateModified: z.coerce.date(),
+ datePublished: z.coerce.date(),
description: z.string(),
draft: z.optional(z.boolean()),
- pubDate: z.coerce.date(),
+ lang: z.string(),
title: z.string(),
}),
});
diff --git a/src/integrations/ogImages.ts b/src/integrations/ogImages.ts
new file mode 100644
index 0000000..5d9146c
--- /dev/null
+++ b/src/integrations/ogImages.ts
@@ -0,0 +1,47 @@
+import type { AstroIntegration } from "astro";
+import { createOgImage } from "../utils/createOgImage";
+import { globby } from "globby";
+import fs from "fs/promises";
+import matter from "gray-matter";
+import path from "path";
+
+const postsDir = path.resolve("./src/content/blog");
+const outDir = path.resolve("./public/images/preview");
+
+export default function ogImageGenerator(): AstroIntegration {
+ return {
+ name: "og-images",
+ hooks: {
+ "astro:build:setup": async ({ logger }) => {
+ await fs.mkdir(outDir, { recursive: true });
+ const mdFiles = await globby("*.md", { cwd: postsDir });
+ logger.info(`${mdFiles.length} posts found`);
+
+ const results = await Promise.allSettled(
+ mdFiles.map(async (file) => {
+ const slug = file.replace(/\.md$/, "");
+ const content = await fs.readFile(path.join(postsDir, file), "utf-8");
+ const { data } = matter(content);
+
+ const png = await createOgImage(data.title, data.datePublished);
+ const outPath = path.join(outDir, `${slug}.png`);
+ await fs.writeFile(outPath, png);
+
+ logger.info(`OG image created: ${slug}`);
+ })
+ );
+
+ results.forEach((r) => {
+ if (r.status === "rejected") {
+ logger.error(`Error for ${r.reason.slug}: ${r.reason.message}`);
+ }
+ });
+
+ const failures = results.filter((r) => r.status === "rejected");
+ if (failures.length) {
+ throw new Error(`Failed to generate OG images for ${failures.length} posts`);
+ }
+ },
+ },
+ };
+}
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index ca8826a..ed3baeb 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -1,26 +1,32 @@
---
+import type { WithContext, Thing } from "schema-dts";
import Analytics from "../components/Analytics.astro";
import Head from "../components/Head.astro";
+import Header from "../components/Header.astro";
import "../scss/global.scss";
type Props = {
- readonly description?: string;
- readonly title?: string;
+ readonly description: string;
+ readonly lang: string;
+ readonly preview: string;
+ readonly schema: WithContext<Thing>;
+ readonly title: string;
};
-const { description, title } = Astro.props;
+const { description, lang, preview, schema, title } = Astro.props;
---
-<html lang="ru">
- <Head
- description={description ?? import.meta.env.DEFAULT_DESCRIPTION}
- title={title ?? import.meta.env.DEFAULT_TITLE}
- />
+<html lang={lang}>
+ <Head title={title} description={description} preview={preview} schema={schema} />
<body>
<main>
+ <section>
+ <Header />
+ </section>
+
<slot />
</main>
- <Analytics title={title ?? import.meta.env.DEFAULT_TITLE} />
+ <Analytics />
</body>
</html>
diff --git a/src/pages/404.astro b/src/pages/404.astro
index e140464..3ec9feb 100644
--- a/src/pages/404.astro
+++ b/src/pages/404.astro
@@ -1,17 +1,30 @@
---
+import { config } from "../config";
import Layout from "../layouts/BaseLayout.astro";
+import pageSchema from "../utils/schemas/pageSchema";
+
+const title = "404 — Page Not Found | Valentin Popov";
+const description = "The page you're looking for doesn't exist!";
+const preview = config.og.defaultPreview;
+const lang = "en";
+
+const schema = pageSchema({
+ siteUrl: new URL("/", Astro.site).toString(),
+ page: "/404",
+ title,
+ description,
+ lang,
+});
---
-<Layout>
- <div style="text-align:center;">
+<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
+ <div style={{ "text-align": "center" }}>
<h1>404</h1>
<p><strong>Page not found</strong></p>
<p>
<small>
If you see this message, please
- <a href=`mailto:valentin@popov.link?subject=${encodeURIComponent('I found a broken page')}`>
- let me know
- </a>
+ <a href=`mailto:valentin@popov.link?subject=${encodeURIComponent('I found a broken page')}`>let me know</a>
</small>
</p>
</div>
diff --git a/src/pages/[...page].astro b/src/pages/[...page].astro
deleted file mode 100644
index 6d513b2..0000000
--- a/src/pages/[...page].astro
+++ /dev/null
@@ -1,33 +0,0 @@
----
-import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
-import { getCollection } from "astro:content";
-import Layout from "../layouts/BaseLayout.astro";
-import Pagination from "../components/Pagination.astro";
-import PostSummary from "../components/PostSummary.astro";
-
-type Props = InferGetStaticPropsType<typeof getStaticPaths>;
-
-export const getStaticPaths = (async ({ paginate }) => {
- const posts = await getCollection("blog", ({ data }) => {
- return data.draft !== true;
- });
-
- posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
-
- return paginate(posts, {
- pageSize: 10,
- });
-}) satisfies GetStaticPaths;
-
-const { page } = Astro.props;
----
-
-<Layout>
- <section style={{ "margin-top": "3rem" }}>
- {page.data.map((post) => <PostSummary post={post} />)}
- </section>
-
- <section>
- <Pagination nextUrl={page.url.next} prevUrl={page.url.prev} />
- </section>
-</Layout>
diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro
index 41b0f5c..d12ff05 100644
--- a/src/pages/blog/[...slug].astro
+++ b/src/pages/blog/[...slug].astro
@@ -2,6 +2,7 @@
import { type CollectionEntry, getCollection } from "astro:content";
import Comments from "../../components/Comments.astro";
import Layout from "../../layouts/BaseLayout.astro";
+import blogPostSchema from "../../utils/schemas/blogPostSchema";
import dayjs from "dayjs";
type Props = CollectionEntry<"blog">;
@@ -18,37 +19,55 @@ export async function getStaticPaths() {
}
const post = Astro.props;
+
const { Content, remarkPluginFrontmatter } = await post.render();
-const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
+
+const description = post.data.description;
+const isBasedOn = post.data.basedOn;
+const lang = post.data.lang;
+const preview = `/images/preview/${post.slug}.png`;
+const slug = post.slug;
+const title = post.data.title;
+
+const dateModified = post.data.dateModified?.toISOString();
+const datePublished = post.data.datePublished.toISOString();
+const formattedDate = dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY");
+
+const schema = blogPostSchema({
+ siteUrl: new URL("/", Astro.site).toString(),
+ dateModified,
+ datePublished,
+ description,
+ isBasedOn,
+ lang,
+ preview,
+ slug,
+ title,
+});
---
<style lang="scss">
- @import "../../scss/_variables.scss";
+ @use "../../scss/variables" as *;
p {
opacity: 0.5;
}
</style>
-<Layout description={post.data.description} title={post.data.title}>
+<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
<article>
- <section>
+ <header>
+ <h1>{title}</h1>
+
<p>
<small>
- <a href="/">&lt; Home</a>
- <span>&nbsp;•&nbsp;</span>
Posted
- <time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
- by&nbsp;{post.data.author}
+ <time datetime={datePublished} lang="en">{formattedDate}</time>
<span>&nbsp;•&nbsp;</span>
<span>{remarkPluginFrontmatter.minutesRead}</span>
</small>
</p>
- </section>
-
- <section>
- <h1>{post.data.title}</h1>
- </section>
+ </header>
<section>
<Content />
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
new file mode 100644
index 0000000..3a27111
--- /dev/null
+++ b/src/pages/blog/index.astro
@@ -0,0 +1,61 @@
+---
+import type { CollectionEntry } from "astro:content";
+import { config } from "../../config";
+import { getCollection } from "astro:content";
+import blogSchema from "../../utils/schemas/blogSchema";
+import Layout from "../../layouts/BaseLayout.astro";
+import PostElement from "../../components/PostElement.astro";
+import RSSIcon from "../../components/Icons/RSS.astro";
+
+const posts = await getCollection("blog", ({ data }) => {
+ return data.draft !== true;
+});
+
+posts.sort((a, b) => b.data.datePublished.getTime() - a.data.datePublished.getTime());
+
+const postsByYear = posts.reduce<Record<string, CollectionEntry<"blog">[]>>((acc, post) => {
+ const year = post.data.datePublished.getFullYear().toString();
+ if (!acc[year]) {
+ acc[year] = [];
+ }
+ acc[year].push(post);
+ return acc;
+}, {});
+
+const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a));
+
+const title = "Valentin Popov's Blog | Software Development, Leadership & Open-Source";
+const description = "Explore Valentin Popov's blog on software development, tech leadership, and open-source experiments. Stay updated with in-depth tutorials and expert insights.";
+const preview = config.og.defaultPreview;
+const lang = "en";
+
+const schema = blogSchema({
+ siteUrl: new URL("/", Astro.site).toString(),
+ title,
+ posts,
+});
+---
+
+<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
+ <section>
+ <h1>
+ Blog posts
+ <RSSIcon />
+ </h1>
+ </section>
+
+ <section>
+ {
+ years.map((year) => (
+ <div>
+ <h2>{year}</h2>
+ <ul>
+ {postsByYear[year].map((post) => (
+ <PostElement post={post} />
+ ))}
+ </ul>
+ </div>
+ ))
+ }
+ </section>
+</Layout>
diff --git a/src/pages/feed.xml.js b/src/pages/feed.xml.js
index d71a020..7c41b4f 100644
--- a/src/pages/feed.xml.js
+++ b/src/pages/feed.xml.js
@@ -2,13 +2,16 @@ import { getCollection } from "astro:content";
import rss from "@astrojs/rss";
export async function GET(context) {
+ const title = "RSS Feed | Valentin Popov Blog";
+ const description = "Follow the latest posts from Valentin Popov via RSS.";
+
const posts = await getCollection("blog", ({ data }) => {
return data.draft !== true;
});
return rss({
- customData: `<language>ru-ru</language>`,
- description: import.meta.env.DEFAULT_DESCRIPTION,
+ customData: `<language>en</language>`,
+ description: description,
items: posts.map((post) => ({
customData: post.data.customData,
description: post.data.description,
@@ -17,6 +20,6 @@ export async function GET(context) {
title: post.data.title,
})),
site: context.site,
- title: import.meta.env.DEFAULT_TITLE,
+ title: title,
});
}
diff --git a/src/pages/index.astro b/src/pages/index.astro
new file mode 100644
index 0000000..b235b9b
--- /dev/null
+++ b/src/pages/index.astro
@@ -0,0 +1,27 @@
+---
+import { config } from "../config";
+import LatestPostsSection from "../components/Sections/LatestPosts.astro";
+import Layout from "../layouts/BaseLayout.astro";
+import pageSchema from "../utils/schemas/pageSchema";
+import SocialLinksSection from "../components/Sections/SocialLinks.astro";
+import WelcomeSection from "../components/Sections/Welcome.astro";
+
+const title = "Valentin Popov – Software Developer & Team Lead | Tech Insights";
+const description = "Blog by Valentin Popov — software developer and team lead writing about code, side projects, digital tools, and fun experiments.";
+const preview = config.og.defaultPreview;
+const lang = "en";
+
+const schema = pageSchema({
+ siteUrl: new URL("/", Astro.site).toString(),
+ page: "/",
+ title,
+ description,
+ lang,
+});
+---
+
+<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
+ <WelcomeSection />
+ <SocialLinksSection />
+ <LatestPostsSection />
+</Layout>
diff --git a/src/scss/_framework.scss b/src/scss/_framework.scss
index 7aa970e..1f836b1 100644
--- a/src/scss/_framework.scss
+++ b/src/scss/_framework.scss
@@ -1,3 +1,5 @@
+@use "variables" as *;
+
*,
*::after,
*::before {
diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss
index 1e0a4ef..039ba4f 100644
--- a/src/scss/_variables.scss
+++ b/src/scss/_variables.scss
@@ -1,5 +1,5 @@
$colorBg: #181818;
-$colorBgAlt: hwb(0deg 0% 100% / 20%);
+$colorBgAlt: rgba(0, 0, 0, 0.2);
$colorBgCode: #3b3d42;
$colorBlossom: #6da13f;
$colorFade: #598332;
diff --git a/src/scss/global.scss b/src/scss/global.scss
index 0e660b1..0c7dbb8 100644
--- a/src/scss/global.scss
+++ b/src/scss/global.scss
@@ -1,3 +1,3 @@
-@import "variables";
-@import "framework";
-@import "print";
+@use "variables";
+@use "framework";
+@use "print";
diff --git a/src/utils/createOgImage.ts b/src/utils/createOgImage.ts
new file mode 100644
index 0000000..da2cece
--- /dev/null
+++ b/src/utils/createOgImage.ts
@@ -0,0 +1,52 @@
+import { config } from "../config";
+import { html } from "satori-html";
+import { resources } from "./ogResources";
+import { Resvg } from "@resvg/resvg-js";
+import dayjs from "dayjs";
+import satori from "satori";
+
+export async function createOgImage(title: string, datePublished: Date): Promise<Buffer> {
+ const formattedDate = dayjs(datePublished).format("MMMM DD, YYYY");
+
+ const markup = await satori(
+ html(`
+<div tw="flex flex-col w-full h-full" style="background-color: ${config.og.color.bg}">
+ <div tw="flex flex-col w-full h-4/5 p-10 justify-center">
+ <div tw="text-2xl mb-6" style="color: ${config.og.color.text}">${formattedDate}</div>
+ <div tw="flex text-6xl w-full font-bold" style="color: ${config.og.color.text}">${title}</div>
+ </div>
+ <div tw="w-full h-1/5 flex p-10 items-center justify-between text-2xl" style="border-top: 1px solid ${config.og.color.bgCode}">
+ <div tw="flex items-center">
+ <span tw="ml-3" style="color: ${config.og.color.text}">${config.og.website.toLocaleUpperCase()}</span>
+ </div>
+ <div tw="flex items-center">
+ <img src="${resources.photoBase64}" tw="w-15 h-15 rounded-full" />
+ <div tw="flex flex-col ml-4">
+ <span style="color: ${config.og.color.text}">${config.author.name}</span>
+ <span style="color: ${config.og.color.blossom}">${config.author.email}</span>
+ </div>
+ </div>
+ </div>
+</div>
+`),
+ {
+ width: config.og.dimensions.width,
+ height: config.og.dimensions.height,
+ fonts: [
+ {
+ name: "Inter",
+ data: resources.fonts.regular,
+ weight: 400,
+ },
+ {
+ name: "Inter",
+ data: resources.fonts.bold,
+ weight: 700,
+ },
+ ],
+ }
+ );
+
+ const image = new Resvg(markup, { fitTo: { mode: "width", value: config.og.dimensions.width } });
+ return image.render().asPng();
+}
diff --git a/src/utils/ogResources.ts b/src/utils/ogResources.ts
new file mode 100644
index 0000000..8049fb2
--- /dev/null
+++ b/src/utils/ogResources.ts
@@ -0,0 +1,15 @@
+import { config } from "../config";
+import fs from "fs/promises";
+import path from "path";
+import sharp from "sharp";
+
+export const resources = {
+ fonts: {
+ regular: await fs.readFile(path.resolve(config.og.fonts.regular)),
+ bold: await fs.readFile(path.resolve(config.og.fonts.bold)),
+ },
+ photoBase64: await (async () => {
+ const buf = await fs.readFile(path.resolve(config.og.photo));
+ return "data:image/png;base64," + (await sharp(buf).resize(120, 120).png({ quality: 95 }).toBuffer()).toString("base64");
+ })(),
+};
diff --git a/src/utils/schemas/blogPostSchema.ts b/src/utils/schemas/blogPostSchema.ts
new file mode 100644
index 0000000..87e1bf2
--- /dev/null
+++ b/src/utils/schemas/blogPostSchema.ts
@@ -0,0 +1,37 @@
+import type { WithContext, BlogPosting } from "schema-dts";
+import { config } from "../../config";
+
+export type BlogPostSchemaParams = {
+ readonly dateModified: string;
+ readonly datePublished: string;
+ readonly description: string;
+ readonly isBasedOn?: string;
+ readonly lang: string;
+ readonly preview: string;
+ readonly siteUrl: string;
+ readonly slug: string;
+ readonly title: string;
+};
+
+export default ({ siteUrl, slug, title, description, preview, datePublished, dateModified, lang, isBasedOn }: BlogPostSchemaParams): WithContext<BlogPosting> => ({
+ "@context": "https://schema.org",
+ "@type": "BlogPosting",
+ "url": new URL(`/blog/${slug}`, siteUrl).toString(),
+ "headline": title,
+ "description": description,
+ "image": new URL(preview, siteUrl).toString(),
+ "datePublished": datePublished,
+ "dateModified": dateModified,
+ "inLanguage": lang,
+ "author": {
+ "@type": "Person",
+ "name": config.author.name,
+ "url": config.author.url,
+ "sameAs": config.author.sameAs,
+ },
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": new URL(`/blog/${slug}`, siteUrl).toString(),
+ },
+ ...(isBasedOn && { isBasedOn: isBasedOn }),
+});
diff --git a/src/utils/schemas/blogSchema.ts b/src/utils/schemas/blogSchema.ts
new file mode 100644
index 0000000..77f4632
--- /dev/null
+++ b/src/utils/schemas/blogSchema.ts
@@ -0,0 +1,26 @@
+import type { WithContext, CollectionPage } from "schema-dts";
+import type { CollectionEntry } from "astro:content";
+
+export type BlogSchemaParams = {
+ readonly posts: CollectionEntry<"blog">[];
+ readonly siteUrl: string;
+ readonly title: string;
+};
+
+export default ({ siteUrl, title, posts }: BlogSchemaParams): WithContext<CollectionPage> => ({
+ "@context": "https://schema.org",
+ "@type": "CollectionPage",
+ "url": new URL("/blog/", siteUrl).toString(),
+ "name": title,
+ "mainEntity": {
+ "@type": "ItemList",
+ "itemListOrder": "https://schema.org/ItemListOrderDescending",
+ "numberOfItems": posts.length,
+ "itemListElement": posts.map((post, index) => ({
+ "@type": "ListItem",
+ "position": index + 1,
+ "url": new URL(`/blog/${post.slug}`, siteUrl).toString(),
+ "name": post.data.title,
+ })),
+ },
+});
diff --git a/src/utils/schemas/pageSchema.ts b/src/utils/schemas/pageSchema.ts
new file mode 100644
index 0000000..606488b
--- /dev/null
+++ b/src/utils/schemas/pageSchema.ts
@@ -0,0 +1,23 @@
+import type { WithContext, WebPage } from "schema-dts";
+
+export type WebsiteSchemaParams = {
+ readonly description: string;
+ readonly page: string;
+ readonly siteUrl: string;
+ readonly title: string;
+ readonly lang: string;
+};
+
+export default ({ siteUrl, page, title, description, lang }: WebsiteSchemaParams): WithContext<WebPage> => ({
+ "@context": "https://schema.org",
+ "@type": "WebPage",
+ "@id": new URL(page, siteUrl).toString(),
+ "url": new URL(page, siteUrl).toString(),
+ "name": title,
+ "description": description,
+ "inLanguage": lang,
+ "mainEntity": {
+ "@type": "WebSite",
+ "@id": new URL("/", siteUrl).toString(),
+ },
+});