0
mirror of https://github.com/valentineus/popov.link.git synced 2025-07-20 07:08:47 +03:00

Compare commits

...

35 Commits

Author SHA1 Message Date
renovate[bot]
ae3176da79 fix(deps): update dependency astro to v5.12.0 (#68)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-17 22:28:10 +00:00
renovate[bot]
3bb4d50767 fix(deps): update dependency astro to v5.11.2 (#67)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 19:34:50 +00:00
renovate[bot]
dd33ec8e48 fix(deps): update all digest updates to v7.1.0 (#66)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 04:08:19 +00:00
renovate[bot]
4698e50cb7 fix(deps): update dependency astro to v5.11.1 (#65)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 16:47:09 +00:00
renovate[bot]
05a0579a91 chore(deps): update mcr.microsoft.com/devcontainers/javascript-node docker tag to v24 (#63) 2025-07-10 21:34:30 +04:00
renovate[bot]
80fcd46280 fix(deps): update dependency sharp to v0.34.3 (#62)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-10 16:29:23 +00:00
renovate[bot]
7634a2e325 fix(deps): update dependency astro to v5.11.0 (#61)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 16:35:07 +00:00
renovate[bot]
29385b1edf fix(deps): update dependency astro to v5.10.2 (#60)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 16:30:49 +00:00
renovate[bot]
ae7dac3099 chore(deps): update dependency prettier to v3.6.2 (#59)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 05:23:58 +00:00
renovate[bot]
d8a59ef4c4 chore(deps): update dependency prettier to v3.6.1 (#58)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 19:13:58 +00:00
renovate[bot]
4412049beb fix(deps): update dependency astro to v5.10.1 (#57)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 19:11:55 +00:00
renovate[bot]
c32aa4e773 chore(deps): update dependency prettier to v3.6.0 (#56)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 07:43:05 +00:00
renovate[bot]
c9d67d9210 fix(deps): update dependency astro to v5.10.0 (#55)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 19:52:42 +00:00
renovate[bot]
d99c901b42 fix(deps): update dependency astro to v5.9.4 (#54)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 17:06:39 +00:00
15fdeb46f1 chore: update README for consistency
- Removed duplicate line regarding project maintenance to streamline the README content.
2025-06-15 12:22:29 +00:00
15795a5337 chore: update GitHub Actions workflow for repository mirroring
- Modified the workflow trigger to include both push and pull request events on the master branch.
- Set permissions for the workflow to allow read access to contents.
2025-06-15 12:02:26 +00:00
bc4f65c1f6 chore: update README and add GitHub Actions workflow for repository mirroring
- Updated the README to change the read-only mirror link to the new URL.
- Added a GitHub Actions workflow for mirroring the repository to a remote Git repository.
2025-06-15 11:51:56 +00:00
a81117972d feat: implement Open Graph image generation and enhance configuration
- Added ogImages integration to generate Open Graph images for blog posts.
- Updated configuration to include Open Graph settings and default preview image.
- Refactored Head component to utilize new preview property for Open Graph meta tags.
- Enhanced blog post schema to include preview image for structured data representation.
- Introduced utility functions for creating Open Graph images with dynamic content.
2025-06-14 19:25:16 +00:00
3d0f485746 feat: add Open Graph and JSON-LD support to Head component
- Introduced OpenGraph component for enhanced social media sharing with Open Graph meta tags.
- Updated Head component to include OpenGraph and JSON-LD for improved SEO and structured data representation.
- Added comments for better clarity on meta tags and JSON-LD integration.
2025-06-14 12:19:01 +00:00
25ebd94466 refactor: remove preview property from blog post schema and configuration
- Eliminated the optional `preview` field from the blog post schema and configuration to streamline data handling.
- Updated the blog post schema to utilize the default image directly from configuration, enhancing consistency in image representation.
2025-06-14 12:12:16 +00:00
0473060773 feat: add configuration and default image for blog posts
- Introduced a new configuration file to centralize author information and default image settings for blog posts.
- Added a default image path in the blog post configuration for improved content presentation.
- Updated blog post schema to utilize the new configuration for author details, enhancing structured data representation.
2025-06-14 12:08:48 +00:00
a65e9c8455 feat: enhance blog post and page schema with new properties
- Added optional `basedOn` field to blog post schema for better content attribution.
- Updated blog post markdown to include `basedOn` reference for improved context.
- Refactored page schema to replace deprecated website schema, enhancing structured data representation.
- Adjusted 404 and index pages to utilize the new page schema for consistency and SEO improvements.
2025-06-14 11:47:17 +00:00
17f9a467d7 refactor: update blog post date handling and schema
- Replaced `pubDate` with `datePublished` in blog post components for consistency.
- Updated sorting logic in blog sections to use `datePublished`.
- Enhanced blog post schema to include `dateModified` for better structured data representation.
- Adjusted various blog markdown files to reflect the new date fields.
2025-06-14 11:25:17 +00:00
3df02c5304 feat: enhance blog post schema and structure
- Added description and lang parameters to the blogPostSchema for improved structured data.
- Updated the blog post layout to include a header section for better semantic structure and accessibility.
2025-06-14 11:09:34 +00:00
9777d996d1 refactor: enhance PostElement structure and update blog schema
- Wrapped the post link in an <article> tag for improved semantic structure.
- Updated blogSchema to include posts for better structured data representation.
- Adjusted the blog index to utilize the new posts parameter for enhanced SEO.
2025-06-14 11:01:42 +00:00
1c15151ef5 fix: enhance accessibility by adding role attribute to SVG icons 2025-06-14 10:53:20 +00:00
renovate[bot]
968b379ff0 fix(deps): update dependency astro to v5.9.3 (#53)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-13 17:56:38 +00:00
renovate[bot]
f408fd6327 chore(deps): update npm to v11.4.2 (#52)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 23:26:10 +00:00
67f245a48f feat: integrate schema.org support and enhance component structure
- Added schema.org support by introducing `schema-dts` for structured data in components.
- Updated `Head`, `BaseLayout`, and various page components to accept and utilize schema props for better SEO.
- Created new utility schemas for blog posts and website to standardize structured data implementation.
- Enhanced the `SocialLinks` section by utilizing dedicated icon components for improved maintainability.
- Refactored the `Header` component to improve accessibility and structure.
- Introduced new icons for social links and integrated them into the layout.
2025-06-11 23:20:36 +00:00
16fa8a3b5d feat: enhance accessibility and language support across components
- Updated various components to include `lang` attributes for improved accessibility and SEO.
- Introduced a new `env.d.ts` file to define environment variables for better type safety.
- Adjusted the print width in `.prettierrc.mjs` for improved code formatting.
- Streamlined the `Header`, `PostElement`, and `SocialLinks` components for better structure and clarity.
- Added language support to blog posts and updated the layout to reflect these changes.
2025-06-11 17:49:14 +00:00
423344fca5 chore: remove unused environment variables and update layout components
- Deleted the .env and src/env.d.ts files as they are no longer needed.
- Updated BaseLayout to require title and description props directly, ensuring better clarity in component usage.
- Adjusted various pages to pass explicit title and description values, enhancing SEO and user experience.
- Increased print width in .prettierrc.mjs for improved code formatting.
2025-06-11 17:20:43 +00:00
78a9c2abc5 feat: add LatestPosts section to homepage
- Introduced a new LatestPosts component to display the five most recent blog posts.
- Updated the index page to include the LatestPosts section, enhancing content visibility.
- Made minor text adjustments in the Welcome section for clarity.
2025-06-11 16:47:48 +00:00
604e507b31 refactor: update blog layout and components
- Removed the PostSummary component and replaced it with a new PostElement component for better post display.
- Introduced SocialLinks and Welcome sections to enhance the homepage layout.
- Updated the index page to utilize the new sections, improving overall structure and user experience.
2025-06-11 16:34:34 +00:00
3d6aedd272 feat: group blog posts by year in index page
- Implemented functionality to categorize blog posts by publication year.
- Updated the blog index page to display posts organized under their respective years for improved navigation.
2025-06-11 16:05:50 +00:00
6fe5df4e32 refactor: simplify Header component by removing site title and navigation wrapper
- Removed the site title and navigation links wrapper from the Header component for a cleaner structure.
- Updated styles to reflect these changes, streamlining the component's layout.
2025-06-11 15:56:06 +00:00
77 changed files with 2362 additions and 526 deletions

View File

@@ -1,5 +1,5 @@
{
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
"image": "mcr.microsoft.com/devcontainers/javascript-node:24",
"forwardPorts": [4321],
"portsAttributes": {
"4321": {

2
.env
View File

@@ -1,2 +0,0 @@
DEFAULT_TITLE=Valentin Popovs Blog
DEFAULT_DESCRIPTION=Tech insights and coding best practices from an OpenSource enthusiast and ethical hacker.

View File

@@ -24,3 +24,5 @@ jobs:
run: npm ci
- name: Run checks
run: npm run check
- name: Run type checks
run: npm run typecheck

25
.github/workflows/mirror.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Mirror
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mirror to remote repository
uses: yesolutions/mirror-action@master
with:
REMOTE: "https://git.popov.link/popov.link.git"
GIT_USERNAME: ${{ secrets.GIT_USERNAME }}
GIT_PASSWORD: ${{ secrets.GIT_PASSWORD }}

View File

@@ -30,7 +30,7 @@ export default {
},
],
plugins: ["prettier-plugin-astro"],
printWidth: 120,
printWidth: 256,
proseWrap: "never",
quoteProps: "consistent",
requirePragma: false,

View File

@@ -41,9 +41,9 @@ npm run preview
## Project Info
- Maintained by [Valentin Popov](mailto:valentin@popov.link)
- Issues: [GitHub](https://github.com/valentineus/popov.link/issues)
- Read-only mirror: [code.popov.link](https://code.popov.link/valentineus/popov.link)
- Read-only mirror: [git.popov.link](https://git.popov.link/popov.link/)
- Maintained by [Valentin Popov](mailto:valentin@popov.link)
## Comments

View File

@@ -1,11 +1,12 @@
import { defineConfig } from "astro/config";
import { remarkReadingTime } from "./src/plugins/remarkReadingTime";
import ogImages from "./src/integrations/ogImages";
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: "https://popov.link",
output: "static",
integrations: [sitemap()],
integrations: [sitemap(), ogImages()],
build: {
inlineStylesheets: "always",
},

2096
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"type": "module",
"version": "2025.01.24",
"private": true,
"packageManager": "npm@11.4.1",
"packageManager": "npm@11.4.2",
"browserslist": [
">0.2%",
"not dead",
@@ -11,6 +11,7 @@
],
"scripts": {
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"dev": "astro dev",
"start": "astro dev",
"check": "astro check",
@@ -22,14 +23,22 @@
"@astrojs/check": "^0.9.4",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.1",
"@resvg/resvg-js": "^2.6.2",
"astro": "^5.9.0",
"autoprefixer": "^10.4.21",
"cssnano": "^7.0.7",
"cssnano-preset-advanced": "^7.0.7",
"dayjs": "^1.11.13",
"geist": "^1.4.2",
"globby": "^14.1.0",
"gray-matter": "^4.0.3",
"mdast-util-to-string": "^4.0.0",
"reading-time": "^1.5.0",
"sass": "^1.89.1",
"satori": "^0.15.2",
"satori-html": "^0.3.2",
"schema-dts": "^1.1.5",
"sharp": "^0.34.2",
"typescript": "^5"
},
"devDependencies": {

BIN
public/images/photo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

2
public/images/preview/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +1 @@
<!-- AppMetrix -->
<script is:inline src="https://appmetrix.com/pixel/T5X0z12SoASBV8Dv"></script>
<script is:inline defer src="https://appmetrix.com/pixel/T5X0z12SoASBV8Dv"></script>

View File

@@ -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}

View File

@@ -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,13 +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>
<!-- 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>

View File

@@ -1,27 +1,6 @@
<style lang="scss">
@use "../scss/variables" as *;
header {
padding-bottom: 1rem;
position: relative;
}
.site-title {
color: $colorText;
font-weight: bold;
left: 0;
position: absolute;
text-decoration: none;
top: 0;
}
.nav-links {
text-align: right;
}
a {
margin-right: 1.5rem;
text-decoration: none;
&:last-child {
margin-right: 0;
@@ -30,9 +9,8 @@
</style>
<header>
<a class="site-title" href="/">{import.meta.env.DEFAULT_TITLE}</a>
<div class="nav-links">
<a href="/">Home</a>
<a href="/blog/">Blog</a>
</div>
<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>

View File

@@ -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" 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>

View File

@@ -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" 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>

View File

@@ -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" 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>

View File

@@ -0,0 +1,17 @@
<style lang="scss">
a {
display: inline-block;
}
svg {
vertical-align: middle;
}
</style>
<a href="/feed.xml" title="RSS Feed" 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>

View File

@@ -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} />

View File

@@ -8,12 +8,18 @@ type Props = {
const { post } = Astro.props;
const { remarkPluginFrontmatter } = await post.render();
const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
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;
@@ -21,12 +27,14 @@ const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY"
</style>
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<div>
<small>
<time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
<span>•</span>
<span>{remarkPluginFrontmatter.minutesRead}</span>
</small>
</div>
<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>

View File

@@ -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">
@use "../scss/variables" as *;
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

29
src/config.ts Normal file
View File

@@ -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",
},
};

View File

@@ -1,8 +1,10 @@
---
basedOn: "https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/"
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."
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/)).

View File

@@ -1,8 +1,9 @@
---
title: "Горячая перезагрузка ElectronJS приложения"
author: "Valentin Popov"
pubDate: "2019-08-15"
description: "Руководство по автоматической перезагрузке приложений на Electron с помощью пакетов electron-reload и electron-webpack. Обход проблем с совместимостью и использование HMR для renderer процесса."
datePublished: "2019-08-15"
dateModified: "2019-08-15"
lang: "ru"
---
## Main процесс

View File

@@ -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
---

View File

@@ -1,8 +1,9 @@
---
title: 'Получение исходного кода "Chromium Projects"'
author: "Valentin Popov"
pubDate: "2012-01-30"
description: "Изучение исходных кодов Chromium: подготовка системы и установка необходимых программных компонентов. Руководство для начинающих разработчиков. Получите инструкции по установке Microsoft Visual Studio, Cygwin, Python и других инструментов. Действительно на январь-февраль 2012 года."
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/)).

View File

@@ -1,8 +1,9 @@
---
title: "Установка Moodle в Fedora"
author: "Valentin Popov"
pubDate: "2018-07-23"
description: "Решение проблем установки Moodle из-за SELinux: как настроить правила доступа для устранения ошибок в веб-интерфейсе и при работе с cURL. Практические советы и команды."
datePublished: "2018-07-23"
dateModified: "2018-07-23"
lang: "ru"
---
Во время установки Moodle, сталкиваешься со следующими проблемами:

View File

@@ -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 сервера.'
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.

View File

@@ -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(),
}),
});

View File

@@ -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`);
}
},
},
};
}

View File

@@ -1,22 +1,23 @@
---
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
title={title ?? import.meta.env.DEFAULT_TITLE}
description={description ?? import.meta.env.DEFAULT_DESCRIPTION}
/>
<html lang={lang}>
<Head title={title} description={description} preview={preview} schema={schema} />
<body>
<main>

View File

@@ -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>

View File

@@ -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,8 +19,31 @@ 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">
@@ -30,23 +54,20 @@ const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY"
}
</style>
<Layout description={post.data.description} title={post.data.title}>
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
<article>
<section>
<h1>{post.data.title}</h1>
</section>
<header>
<h1>{title}</h1>
<section>
<p>
<small>
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>
</header>
<section>
<Content />

View File

@@ -1,19 +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.pubDate.getTime() - a.data.pubDate.getTime());
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>
<section style={{ "margin-top": "3rem" }}>
<ul>
{posts.map((post) => <PostElement post={post} />)}
</ul>
<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>

View File

@@ -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,
});
}

View File

@@ -1,17 +1,27 @@
---
import { getCollection } from "astro:content";
import { config } from "../config";
import LatestPostsSection from "../components/Sections/LatestPosts.astro";
import Layout from "../layouts/BaseLayout.astro";
import PostSummary from "../components/PostSummary.astro";
import pageSchema from "../utils/schemas/pageSchema";
import SocialLinksSection from "../components/Sections/SocialLinks.astro";
import WelcomeSection from "../components/Sections/Welcome.astro";
const posts = await getCollection("blog", ({ data }) => {
return data.draft !== true;
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,
});
posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
---
<Layout>
<section style={{ "margin-top": "3rem" }}>
{posts.map((post) => <PostSummary post={post} />)}
</section>
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
<WelcomeSection />
<SocialLinksSection />
<LatestPostsSection />
</Layout>

View File

@@ -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();
}

15
src/utils/ogResources.ts Normal file
View File

@@ -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");
})(),
};

View File

@@ -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 }),
});

View File

@@ -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,
})),
},
});

View File

@@ -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(),
},
});