Skip to content

Commit 1660d14

Browse files
committed
Improve SEO and blog entry display
1 parent d97fa00 commit 1660d14

12 files changed

Lines changed: 383 additions & 142 deletions

File tree

astro.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import remarkCustomHeaderId from 'remark-custom-header-id';
77

88
// https://astro.build/config
99
export default defineConfig({
10-
site: "https://linwood.dev",
10+
site: "https://www.linwood.dev",
1111
image: {
1212
responsiveStyles: true,
1313
layout: "constrained",

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
"@astrojs/sitemap": "3.7.2",
1616
"@linwooddev/style": "^0.4.2",
1717
"@phosphor-icons/web": "^2.1.2",
18-
"astro": "6.1.9",
18+
"astro": "6.2.1",
1919
"remark-custom-header-id": "^1.0.0",
2020
"sharp": "^0.34.5",
2121
"typescript": "^6.0.3",
2222
"workbox-build": "^7.4.0"
2323
},
24-
"packageManager": "pnpm@11.0.0",
24+
"packageManager": "pnpm@11.0.5",
2525
"devDependencies": {
2626
"@vite-pwa/astro": "^1.2.0",
2727
"sass": "^1.99.0",

pnpm-lock.yaml

Lines changed: 115 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/robots.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
User-agent: *
2+
Allow: /
3+
4+
Sitemap: https://www.linwood.dev/sitemap-index.xml

src/blog.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,50 @@
11
import { type CollectionEntry, getCollection } from "astro:content";
22

3+
const stripMarkdown = (value: string) =>
4+
value
5+
.replace(/^import\s.+$/gm, "")
6+
.replace(/<[^>]+>/g, " ")
7+
.replace(/\{#[^}]+\}/g, "")
8+
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
9+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
10+
.replace(/[`*_>#-]/g, "")
11+
.replace(/\s+/g, " ")
12+
.trim();
13+
314
export const getEntryProject = (entry: CollectionEntry<"blog">) => {
415
return getProject(getEntryProjectName(entry));
516
};
617
export const getEntryProjectName = (entry: CollectionEntry<"blog">) => {
7-
const projectName = entry.id.substring(0, entry.id.indexOf("/"));
18+
const projectName = getEntryUrl(entry).split("/")[0];
819
return projectName;
920
};
1021

1122
export const getEntryUrl = (entry: CollectionEntry<"blog">) => {
12-
return entry.id;
23+
return entry.data.slug || entry.id;
24+
};
25+
26+
export const getEntryDescription = (entry: CollectionEntry<"blog">) => {
27+
if (entry.data.description) return entry.data.description;
28+
29+
const body = (entry as CollectionEntry<"blog"> & { body?: string }).body;
30+
const firstParagraph =
31+
body
32+
?.split(/\n{2,}/)
33+
.map(stripMarkdown)
34+
.find((paragraph) => paragraph.length > 0) ?? entry.data.title;
35+
36+
return firstParagraph.length > 160
37+
? `${firstParagraph.slice(0, 157).trim()}...`
38+
: firstParagraph;
1339
};
1440

1541
export const getProjects = async (
1642
force?: boolean
1743
): Promise<CollectionEntry<"projects">[]> => {
1844
const projects = await getCollection("projects");
19-
return projects.filter((project) => !project.data.unlisted || force).sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
45+
return projects
46+
.filter((project) => !project.data.unlisted || force)
47+
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
2048
};
2149

2250
export const getProject = async (

src/components/BaseHead.astro

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,24 @@ export interface Props {
77
title: string;
88
description: string;
99
image?: string;
10+
type?: "website" | "article";
11+
publishedTime?: Date;
12+
modifiedTime?: Date;
13+
author?: string;
1014
}
1115
1216
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
1317
14-
const { title, description, image } = Astro.props;
18+
const {
19+
title,
20+
description,
21+
image,
22+
type = "website",
23+
publishedTime,
24+
modifiedTime,
25+
author,
26+
} = Astro.props;
27+
const imageURL = image ? new URL(image, Astro.site) : undefined;
1528
---
1629

1730
<!-- Global Metadata -->
@@ -27,20 +40,29 @@ const { title, description, image } = Astro.props;
2740
<title>{title}</title>
2841
<meta name="title" content={title} />
2942
<meta name="description" content={description} />
43+
<meta name="robots" content="index, follow, max-image-preview:large" />
44+
<meta name="theme-color" content="#35ef7b" />
45+
<link rel="alternate" type="application/rss+xml" title="Linwood Blog" href="/rss.xml" />
3046

3147
<!-- Open Graph / Facebook -->
32-
<meta property="og:type" content="website" />
33-
<meta property="og:url" content={Astro.url} />
48+
<meta property="og:type" content={type} />
49+
<meta property="og:url" content={canonicalURL} />
50+
<meta property="og:site_name" content="Linwood" />
51+
<meta property="og:locale" content="en_US" />
3452
<meta property="og:title" content={title} />
3553
<meta property="og:description" content={description} />
36-
{image && <meta property="og:image" content={new URL(image, Astro.url)} />}
54+
{imageURL && <meta property="og:image" content={imageURL} />}
55+
{imageURL && <meta property="og:image:alt" content={title} />}
56+
{publishedTime && <meta property="article:published_time" content={publishedTime.toISOString()} />}
57+
{modifiedTime && <meta property="article:modified_time" content={modifiedTime.toISOString()} />}
58+
{author && <meta property="article:author" content={author} />}
3759

3860
<!-- Twitter -->
39-
<meta property="twitter:card" content="summary_large_image" />
40-
<meta property="twitter:url" content={Astro.url} />
41-
<meta property="twitter:title" content={title} />
42-
<meta property="twitter:description" content={description} />
43-
{image && <meta property="twitter:image" content={new URL(image, Astro.url)} />}
61+
<meta name="twitter:card" content="summary_large_image" />
62+
<meta name="twitter:url" content={canonicalURL} />
63+
<meta name="twitter:title" content={title} />
64+
<meta name="twitter:description" content={description} />
65+
{imageURL && <meta name="twitter:image" content={imageURL} />}
4466
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
4567

4668
<ClientRouter />

src/components/BlogList.astro

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
import type { CollectionEntry } from "astro:content";
3-
import { getEntryUrl } from "../blog";
4-
import BaseHead from "./BaseHead.astro";
3+
import { getEntry } from "astro:content";
4+
import { getEntryDescription, getEntryUrl } from "../blog";
55
import FormattedDate from "./FormattedDate.astro";
66
import Link from "./Link.astro";
77
import type { Page } from "astro";
@@ -13,6 +13,12 @@ export interface Props {
1313
1414
const { page, isMain } = Astro.props as Props;
1515
const posts = page.data;
16+
const postsWithAuthors = await Promise.all(
17+
posts.map(async (post) => ({
18+
post,
19+
author: await getEntry("authors", post.data.author.id),
20+
}))
21+
);
1622
---
1723

1824
<section>
@@ -28,18 +34,42 @@ const posts = page.data;
2834
}
2935
<ul class="col gap-md align-stretch pv-md ph-xs">
3036
{
31-
posts.map((post) => (
37+
postsWithAuthors.map(({ post, author }) => (
3238
<li>
3339
<Link
3440
variant="secondary"
3541
href={`/${getEntryUrl(post)}`}
3642
look="card"
37-
class="row align-center wrap w-full p-sm no-decoration"
43+
class="blog-list-item w-full p-sm no-decoration"
3844
>
39-
<div class="w-md h-full bold">
40-
<FormattedDate date={post.data.date} />
45+
<div class="blog-list-meta">
46+
<div class="blog-list-date bold">
47+
<FormattedDate date={post.data.date} />
48+
</div>
49+
{
50+
author && (
51+
<p class="blog-list-author m-none">
52+
<span class="blog-list-avatar">
53+
<img
54+
src={author.data.avatar}
55+
width={32}
56+
height={32}
57+
alt=""
58+
loading="lazy"
59+
decoding="async"
60+
/>
61+
</span>
62+
<span>{author.data.name}</span>
63+
</p>
64+
)
65+
}
66+
</div>
67+
<div class="blog-list-content">
68+
<p class="highlight m-none">{post.data.title}</p>
69+
<p class="blog-list-description m-none">
70+
{getEntryDescription(post)}
71+
</p>
4172
</div>
42-
<p class="highlight">{post.data.title}</p>
4373
</Link>
4474
</li>
4575
))
@@ -67,3 +97,76 @@ const posts = page.data;
6797
</div>
6898
</div>
6999
</section>
100+
101+
<style>
102+
.blog-list-item {
103+
display: grid;
104+
grid-template-columns: 10rem minmax(0, 1fr);
105+
align-items: center;
106+
column-gap: 1.5rem;
107+
row-gap: 0.35rem;
108+
}
109+
110+
.blog-list-meta {
111+
display: flex;
112+
flex-direction: column;
113+
gap: 0.4rem;
114+
min-width: 0;
115+
}
116+
117+
.blog-list-date {
118+
white-space: nowrap;
119+
}
120+
121+
.blog-list-content {
122+
min-width: 0;
123+
display: flex;
124+
flex-direction: column;
125+
gap: 0.15rem;
126+
}
127+
128+
.blog-list-author {
129+
display: inline-flex;
130+
align-items: center;
131+
gap: 0.4rem;
132+
color: var(--ls-background-text);
133+
font-size: 0.9rem;
134+
opacity: 0.72;
135+
}
136+
137+
.blog-list-avatar {
138+
display: inline-flex;
139+
align-items: center;
140+
justify-content: center;
141+
width: 1.35rem;
142+
height: 1.35rem;
143+
aspect-ratio: 1;
144+
border-radius: 50%;
145+
overflow: hidden;
146+
flex: 0 0 auto;
147+
}
148+
149+
.blog-list-avatar img {
150+
display: block;
151+
width: 100% !important;
152+
height: 100% !important;
153+
max-width: none;
154+
border-radius: inherit;
155+
object-fit: cover;
156+
}
157+
158+
.blog-list-description {
159+
color: var(--ls-background-text);
160+
opacity: 0.82;
161+
overflow: hidden;
162+
text-overflow: ellipsis;
163+
white-space: nowrap;
164+
}
165+
166+
@media (max-width: 640px) {
167+
.blog-list-item {
168+
grid-template-columns: 1fr;
169+
align-items: start;
170+
}
171+
}
172+
</style>

src/content.config.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { file, glob } from "astro/loaders";
22
import { defineCollection, reference } from "astro:content";
3-
import { z } from 'astro/zod';
3+
import { z } from "astro/zod";
44

55
const blog = defineCollection({
6-
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
6+
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
77
// Type-check frontmatter using a schema
88
schema: z.object({
99
title: z.string(),
1010
description: z.string().optional(),
11+
slug: z.string().optional(),
12+
tags: z.array(z.string()).default([]),
1113
// Transform string to Date object
1214
date: z
1315
.string()
@@ -19,7 +21,7 @@ const blog = defineCollection({
1921
}),
2022
});
2123
const projects = defineCollection({
22-
loader: glob({ base: './src/content/projects', pattern: '**/*.json' }),
24+
loader: glob({ base: "./src/content/projects", pattern: "**/*.json" }),
2325
schema: z.object({
2426
title: z.string(),
2527
description: z.string(),
@@ -32,7 +34,7 @@ const projects = defineCollection({
3234
}),
3335
});
3436
const authors = defineCollection({
35-
loader: glob({ base: './src/content/authors', pattern: '**/*.json' }),
37+
loader: glob({ base: "./src/content/authors", pattern: "**/*.json" }),
3638
schema: z.object({
3739
name: z.string(),
3840
avatar: z.url(),
@@ -41,8 +43,7 @@ const authors = defineCollection({
4143
});
4244

4345
const libraries = defineCollection({
44-
loader: file("src/content/libraries.json", {
45-
}),
46+
loader: file("src/content/libraries.json", {}),
4647
schema: z.object({
4748
id: z.string(),
4849
description: z.string(),

0 commit comments

Comments
 (0)