Complete a blog tutorial

This commit is contained in:
Daisuke Nakahara 2025-01-14 21:17:35 +09:00
parent a14cb236e3
commit 4375b7c2e4
26 changed files with 2489 additions and 538 deletions

View file

@ -1,5 +1,10 @@
// @ts-check
import { defineConfig } from 'astro/config';
import preact from "@astrojs/preact";
// https://astro.build/config
export default defineConfig({});
export default defineConfig({
site: "https://example.com",
integrations: [preact()]
});

2373
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,12 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.1.2"
"@astrojs/preact": "^4.0.1",
"@astrojs/rss": "^4.0.11",
"astro": "^5.1.5",
"preact": "^10.25.4"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}

View file

@ -10,8 +10,6 @@ tags: ["astro", "blogging", "learning in public"]
---
# My First Blog Post
Published on: 2022-07-01
Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
## What I've accomplished

View file

@ -0,0 +1,5 @@
---
const { title, url } = Astro.props;
---
<li><a href={url}>{title}</a></li>

View file

@ -0,0 +1,15 @@
---
import Social from "./Social.astro";
---
<style>
footer {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
</style>
<footer>
<Social platform="github" username="Daisuke897" />
</footer>

View file

@ -0,0 +1,9 @@
---
---
<div class="hamburger">
<span class="line"></span>
<span class="line"></span>
<span class="line"></span>
</div>

View file

@ -0,0 +1,13 @@
---
import Hamburger from "./Hamburger.astro";
import Navigation from "./Navigation.astro";
import ThemeIcon from './ThemeIcon.astro';
---
<header>
<nav>
<Hamburger />
<ThemeIcon />
<Navigation />
</nav>
</header>

View file

@ -0,0 +1,10 @@
---
---
<div class="nav-links">
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Blog</a>
<a href="/tags/">Tags</a>
</div>

View file

@ -0,0 +1,14 @@
---
const { platform, username } = Astro.props;
---
<a href={`https://www.${platform}.com/${username}`}>{platform}</a>
<style>
a {
padding: 0.5rem 1rem;
color: white;
background-color: #4c1d95;
text-decoration: none;
}
</style>

View file

@ -0,0 +1,73 @@
---
---
<button id="themeToggle">
<svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
</button>
<style>
#themeToggle {
border: 0;
background: none;
}
.sun {
fill: black;
}
.moon {
fill: transparent;
}
:global(.dark) .sun {
fill: transparent;
}
:global(.dark) .moon {
fill: white;
}
</style>
<script is:inline>
const theme = (() => {
if (
typeof localStorage !== "undefined" &&
localStorage.getItem("theme")
) {
return localStorage.getItem("theme") ?? "light";
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();
if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
window.localStorage.setItem("theme", theme);
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
};
document
.getElementById("themeToggle")
?.addEventListener("click", handleToggleClick);
</script>

21
src/content.config.ts Normal file
View file

@ -0,0 +1,21 @@
// Import the glob loader
import { glob } from "astro/loaders";
// Import utilities from `astro:content`
import { z, defineCollection } from "astro:content";
// Define a `loader` and `schema` for each collection
const blog = defineCollection({
loader: glob({ pattern: '**/[^_]*.md', base: "./src/blog" }),
schema: z.object({
title: z.string(),
pubDate: z.date(),
description: z.string(),
author: z.string(),
image: z.object({
url: z.string(),
alt: z.string()
}),
tags: z.array(z.string())
})
});
// Export a single `collections` object to register your collection(s)
export const collections = { blog };

View file

@ -0,0 +1,25 @@
---
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import "../styles/global.css";
const { pageTitle } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{pageTitle}</title>
</head>
<body>
<Header />
<h1>{pageTitle}</h1>
<slot />
<Footer />
<script>
import "../scripts/menu.js";
</script>
</body>
</html>

View file

@ -0,0 +1,41 @@
---
import BaseLayout from "./BaseLayout.astro";
const { frontmatter } = Astro.props;
---
<BaseLayout pageTitle={frontmatter.title}>
<p>{frontmatter.pubDate.toLocaleDateString()}</p>
<p><em>{frontmatter.description}</em></p>
<p>Written by: {frontmatter.author}</p>
<img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} />
<div class="tags">
{
frontmatter.tags.map((tag: string) => (
<p class="tag">
<a href={`/tags/${tag}`}>{tag}</a>
</p>
))
}
</div>
<slot />
</BaseLayout>
<style>
a {
color: #00539f;
}
.tags {
display: flex;
flex-wrap: wrap;
}
.tag {
margin: 0.25em;
border: dotted 1px #a1a1a1;
border-radius: 0.5em;
padding: 0.5em 1em;
font-size: 1.15em;
background-color: #f8fcfd;
}
</style>

View file

@ -1,4 +1,5 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
const pageTitle = "About Me";
const identity = {
@ -11,47 +12,89 @@ const identity = {
const skills = ["HTML", "CSS", "JavaScript", "React", "Astro", "Writing Docs"];
const happy = true;
const finished = false;
const finished = true;
const goal = 3;
const skillColor = "navy";
const fontWeight = "bold";
const textCase = "uppercase";
---
<html lang="en">
<!-- <html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{pageTitle}</title>
<style define:vars={{skillColor, fontWeight, textCase}}>
h1 {
color: purple;
font-size: 4rem;
}
.skill {
color: var(--skillColor);
font-weight: var(--fontWeight);
text-transform: var(--textCase);
}
</style>
</head>
<body>
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Blog</a>
<h1>{pageTitle}</h1>
<Header />
<h1>{pageTitle}</h1> -->
<BaseLayout pageTitle={pageTitle}>
<style is:global define:vars={{ skillColor, fontWeight, textCase }}>
h1 {
color: purple;
font-size: 4rem;
}
.skill {
color: var(--skillColor);
font-weight: var(--fontWeight);
text-transform: var(--textCase);
}
</style>
<h2>... and my new Astro site!</h2>
<p>I am working through Astro's introductory tutorial. This is the second page on my website, and it's the first one I built myself!</p>
<p>
I am working through Astro's introductory tutorial. This is the second
page on my website, and it's the first one I built myself!
</p>
<p>This site will update as I complete more of the tutorial, so keep checking back and see how my journey is going!</p>
<p>
This site will update as I complete more of the tutorial, so keep
checking back and see how my journey is going!
</p>
<p>Here are a few facts about me:</p>
<ul>
<li>My name is {identity.firstName}.</li>
<li>I live in {identity.country} and I work as a {identity.occupation}.</li>
{identity.hobbies.length >= 2 &&
<li>Two of my hobbies are: {identity.hobbies[0]} and {identity.hobbies[1]}</li>
<li>
I live in {identity.country} and I work as a {identity.occupation}.
</li>
{
identity.hobbies.length >= 2 && (
<li>
Two of my hobbies are: {identity.hobbies[0]} and{" "}
{identity.hobbies[1]}
</li>
)
}
</ul>
<p>My skills are:</p>
<ul>
{skills.map((skill) => <li>{skill}</li>)}
{skills.map((skill) => <li class="skill">{skill}</li>)}
</ul>
{happy && <p>I am happy to be learning Astro!</p>}
{finished && <p>I finished this tutorial!</p>}
{goal === 3 ? <p>My goal is to finish in 3 days.</p> : <p>My goal is not 3 days.</p>}
</body>
</html>
{
goal === 3 ? (
<p>My goal is to finish in 3 days.</p>
) : (
<p>My goal is not 3 days.</p>
)
}
</BaseLayout>

View file

@ -1,26 +1,21 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "../layouts/BaseLayout.astro";
import BlogPost from "../components/BlogPost.astro";
const allPosts = await getCollection("blog");
const numberOfPosts = allPosts.length;
const pageTitle = "My Astro Learning Blog";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{pageTitle}</title>
</head>
<body>
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Blog</a>
<h1>{pageTitle}</h1>
<BaseLayout pageTitle={pageTitle}>
<p>This is where I will post about my journey learning Astro.</p>
<p>I have written {numberOfPosts} blog posts.</p>
<ul>
<li><a href="/posts/post-1/">Post 1</a></li>
<li><a href="/posts/post-2/">Post 2</a></li>
<li><a href="/posts/post-3/">Post 3</a></li>
{
allPosts.map((post: any) => (
<BlogPost url={`/posts/${post.id}/`} title={post.data.title} />
))
}
</ul>
</body>
</html>
</BaseLayout>

View file

@ -1,20 +1,8 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
const pageTitle = "Home Page";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{pageTitle}</title>
</head>
<body>
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Blog</a>
<h1>{pageTitle}</h1>
</body>
</html>
<BaseLayout pageTitle={pageTitle}>
<h2>ブログ</h2>
</BaseLayout>

View file

@ -0,0 +1,19 @@
---
import { getCollection, render } from "astro:content";
import MarkdownPostLayout from "../../layouts/MarkdownPostLayout.astro";
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post: any) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<MarkdownPostLayout frontmatter={post.data}>
<Content />
</MarkdownPostLayout>

View file

@ -1,11 +0,0 @@
---
title: My Second Blog Post
author: Astro Learner
description: "After learning some Astro, I couldn't stop!"
image:
url: "https://docs.astro.build/assets/arc.webp"
alt: "The Astro logo on a dark background with a purple gradient arc."
pubDate: 2022-07-08
tags: ["astro", "blogging", "learning in public", "successes"]
---
After a successful first week learning Astro, I decided to try some more. I wrote and imported a small component from memory!

View file

@ -1,11 +0,0 @@
---
title: My Third Blog Post
author: Astro Learner
description: "I had some challenges, but asking in the community really helped!"
image:
url: "https://docs.astro.build/assets/rays.webp"
alt: "The Astro logo on a dark background with rainbow rays."
pubDate: 2022-07-15
tags: ["astro", "learning in public", "setbacks", "community"]
---
It wasn't always smooth sailing, but I'm enjoying building with Astro. And, the [Discord community](https://astro.build/chat) is really friendly and helpful!

18
src/pages/rss.xml.js Normal file
View file

@ -0,0 +1,18 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection("blog");
return rss({
title: 'Astro Learner | Blog',
description: 'My journey learning Astro',
site: context.site,
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/posts/${post.id}/`,
})),
customData: `<language>en-us</language>`,
});
}

View file

@ -0,0 +1,33 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
import BlogPost from "../../components/BlogPost.astro";
export async function getStaticPaths() {
const allPosts = await getCollection("blog");
const uniqueTags = [...new Set(allPosts.map((post: any) => post.data.tags).flat())];
return uniqueTags.map((tag) => {
const filteredPosts = allPosts.filter((post: any) =>
post.data.tags.includes(tag),
);
return {
params: { tag },
props: { posts: filteredPosts },
};
});
}
const { tag } = Astro.params;
const { posts } = Astro.props;
---
<BaseLayout pageTitle={tag}>
<p>Posts tagged with {tag}</p>
<ul>
{
posts.map((post: any) => (
<BlogPost url={`/posts/${post.id}/`} title={post.data.title} />
))
}
</ul>
</BaseLayout>

View file

@ -0,0 +1,39 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
const allPosts = await getCollection("blog");
const tags = [...new Set(allPosts.map((post: any) => post.data.tags).flat())];
const pageTitle = "Tag Index";
---
<BaseLayout pageTitle={pageTitle}>
<div class="tags">
{
tags.map((tag) => (
<p class="tag">
<a href={`/tags/${tag}`}>{tag}</a>
</p>
))
}
</div>
</BaseLayout>
<style>
a {
color: #00539f;
}
.tags {
display: flex;
flex-wrap: wrap;
}
.tag {
margin: 0.25em;
border: dotted 1px #a1a1a1;
border-radius: 0.5em;
padding: 0.5em 1em;
font-size: 1.15em;
background-color: #f8fcfd;
}
</style>

3
src/scripts/menu.js Normal file
View file

@ -0,0 +1,3 @@
document.querySelector('.hamburger').addEventListener('click', () => {
document.querySelector('.nav-links').classList.toggle('expanded');
});

92
src/styles/global.css Normal file
View file

@ -0,0 +1,92 @@
html {
background-color: #f1f5f9;
font-family: sans-serif;
}
body {
margin: 0 auto;
width: 100%;
max-width: 80ch;
padding: 1rem;
line-height: 1.5;
}
* {
box-sizing: border-box;
}
h1 {
margin: 1rem 0;
font-size: 2.5rem;
}
/* nav styles */
.hamburger {
padding-right: 20px;
cursor: pointer;
}
.hamburger .line {
display: block;
width: 40px;
height: 5px;
margin-bottom: 10px;
background-color: #ff9776;
}
.nav-links {
width: 100%;
top: 5rem;
left: 48px;
background-color: #ff9776;
display: none;
margin: 0;
}
.nav-links a {
display: block;
text-align: center;
padding: 10px 0;
text-decoration: none;
font-size: 1.2rem;
font-weight: bold;
text-transform: uppercase;
}
.nav-links a:hover,
.nav-links a:focus {
background-color: #ff9776;
}
.expanded {
display: unset;
}
@media screen and (min-width: 636px) {
.nav-links {
margin-left: 5em;
display: block;
position: static;
width: auto;
background: none;
}
.nav-links a {
display: inline-block;
padding: 15px 20px;
}
.hamburger {
display: none;
}
}
html.dark {
background-color: #0d0950;
color: #fff;
}
.dark .nav-links a {
color: #fff;
}

View file

@ -1,5 +1,14 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}