Add footer navigation and contact page
The changes include adding navigation links to the footer, creating a new contact page, and updating feed generation to use a common sorting function.
keypair34 committed
Sep 8, 2025 at 14:53 UTC
065eb1a7c7b9d8fd830e4ab9893871a76c94d4b5
4 files changed
+170
-27
src/components/layout/Footer.astro
+70
-12
@@ -5,17 +5,26 @@ const today = new Date()
5
6
<footer>
7
<div class="footer-content">
8
- <div class="copyright">
9
- <span class="date">
10
- ©
11
- {today.getFullYear()}
12
- </span>
13
- {SITE.author}
8
+ <div class="footer-links">
9
+ <nav class="footer-nav">
10
+ <a href="/contact">Contact</a>
11
+ <a href="/privacy-policy">Privacy Policy</a>
12
+ <a href="/terms-of-use">Terms of Use</a>
13
+ </nav>
14
</div>
15
- <div class="powered-by">
16
- Powered by{' '}
17
- <a href="https://smbcloud.xyz" target="__blank">smbCloud</a> |{' '}
18
- <a href="https://github.com/smbcloudXYZ/pepakbasajawa-web" target="__blank">Source</a>
15
+ <div class="footer-bottom">
16
+ <div class="copyright">
17
+ <span class="date">
18
+ ©
19
+ {today.getFullYear()}
20
+ </span>
21
+ {SITE.author}
22
+ </div>
23
+ <div class="powered-by">
24
+ Powered by{' '}
25
+ <a href="https://smbcloud.xyz" target="__blank">smbCloud</a> |{' '}
26
+ <a href="https://github.com/smbcloudXYZ/pepakbasajawa-web" target="__blank">Source</a>
27
+ </div>
28
</div>
29
</div>
30
</footer>
@@ -29,6 +38,38 @@ const today = new Date()
38
}
39
40
.footer-content {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 1.5rem;
44
+ width: 100%;
45
+ }
46
+
47
+ .footer-links {
48
+ padding-bottom: 1rem;
49
+ border-bottom: 1px solid var(--border);
50
+ }
51
+
52
+ .footer-nav {
53
+ display: flex;
54
+ gap: 1.5rem;
55
+ justify-content: center;
56
+ flex-wrap: wrap;
57
+ }
58
+
59
+ .footer-nav a {
60
+ color: var(--text-secondary);
61
+ text-decoration: none;
62
+ padding: 0.25rem 0.5rem;
63
+ border-radius: 0.25rem;
64
+ transition: all 0.2s ease-out;
65
+ }
66
+
67
+ .footer-nav a:hover {
68
+ color: var(--text-primary);
69
+ background-color: var(--code-bg);
70
+ }
71
+
72
+ .footer-bottom {
73
display: flex;
74
justify-content: space-between;
75
align-items: center;
@@ -42,13 +83,30 @@ const today = new Date()
83
white-space: nowrap;
84
}
85
45
- footer a {
86
+ .powered-by a {
87
color: var(--text-secondary);
88
text-decoration: none;
89
transition: color 0.2s ease-out;
90
}
91
51
- footer a:hover {
92
+ .powered-by a:hover {
93
color: var(--text-primary);
94
}
95
+
96
+ @media (max-width: 768px) {
97
+ .footer-nav {
98
+ gap: 1rem;
99
+ }
100
+
101
+ .footer-bottom {
102
+ flex-direction: column;
103
+ gap: 0.75rem;
104
+ text-align: center;
105
+ }
106
+
107
+ .copyright,
108
+ .powered-by {
109
+ white-space: normal;
110
+ }
111
+ }
112
</style>
src/pages/contact.astro
new
+93
@@ -0,0 +1,93 @@
1
+---
2
+import IndexLayout from '@/layouts/IndexLayout.astro'
3
+import { SITE } from '@/config'
4
+---
5
+
6
+<IndexLayout title={`Contact - ${SITE.title}`} description="Get in touch with PepakBasaJawa">
7
+ <div class="contact-page prose">
8
+ <h1>Contact Us</h1>
9
+
10
+ <p>
11
+ Have questions about learning Javanese language? Want to contribute to our content? We'd love
12
+ to hear from you!
13
+ </p>
14
+
15
+ <div class="contact-info">
16
+ <h2>Get in Touch</h2>
17
+ <p>Feel free to reach out to us via email:</p>
18
+
19
+ <div class="email-section">
20
+ <a href="mailto:splitfire@setoelkahfi.se" class="email-link"> splitfire@setoelkahfi.se </a>
21
+ </div>
22
+
23
+ <p>We'll do our best to respond to your inquiry as soon as possible.</p>
24
+ </div>
25
+
26
+ <div class="topics">
27
+ <h2>What can you contact us about?</h2>
28
+ <ul>
29
+ <li>Questions about Javanese language learning materials</li>
30
+ <li>Suggestions for new content or improvements</li>
31
+ <li>Technical issues with the website</li>
32
+ <li>Collaboration opportunities</li>
33
+ <li>General feedback about PepakBasaJawa</li>
34
+ </ul>
35
+ </div>
36
+ </div>
37
+</IndexLayout>
38
+
39
+<style>
40
+ .contact-page {
41
+ max-width: 100%;
42
+ margin: 0 auto;
43
+ padding: 2rem 0;
44
+ }
45
+
46
+ .contact-info {
47
+ margin: 2rem 0;
48
+ }
49
+
50
+ .email-section {
51
+ margin: 1.5rem 0;
52
+ padding: 1rem;
53
+ background: var(--code-bg);
54
+ border-radius: 8px;
55
+ border-left: 4px solid var(--accent);
56
+ }
57
+
58
+ .email-link {
59
+ font-size: 1.25rem;
60
+ font-weight: 600;
61
+ color: var(--accent);
62
+ text-decoration: none;
63
+ word-break: break-all;
64
+ }
65
+
66
+ .email-link:hover {
67
+ text-decoration: underline;
68
+ }
69
+
70
+ .topics {
71
+ margin: 2rem 0;
72
+ }
73
+
74
+ .topics ul {
75
+ list-style-type: disc;
76
+ margin-left: 1.5rem;
77
+ }
78
+
79
+ .topics li {
80
+ margin: 0.5rem 0;
81
+ line-height: 1.6;
82
+ }
83
+
84
+ @media (max-width: 768px) {
85
+ .contact-page {
86
+ padding: 1rem 0;
87
+ }
88
+
89
+ .email-link {
90
+ font-size: 1.1rem;
91
+ }
92
+ }
93
+</style>
src/utils/draft.ts
+3
-2
@@ -1,11 +1,12 @@
1
import { getCollection, type CollectionEntry } from 'astro:content'
2
3
/**
4
- * Get all posts, filtering out posts whose filenames start with _
4
+ * Get all posts, filtering out posts whose filenames start with _ and static pages
5
*/
6
export async function getFilteredPosts() {
7
const posts = await getCollection('posts')
8
- return posts.filter((post) => !post.id.startsWith('_'))
8
+ const staticPages = ['privacy-policy', 'terms-of-use']
9
+ return posts.filter((post) => !post.id.startsWith('_') && !staticPages.includes(post.id))
10
}
11
12
/**
src/utils/feed.ts
+4
-13
@@ -1,14 +1,10 @@
1
-import { getCollection, type CollectionEntry } from 'astro:content'
1
+import type { CollectionEntry } from 'astro:content'
2
import { SITE } from '@/config'
3
import type { APIContext } from 'astro'
4
+import { getSortedFilteredPosts } from '@/utils/draft'
5
6
export async function generateRSS(context: APIContext) {
6
- const posts = await getCollection('posts')
7
- const filteredPosts = posts.filter((post) => !post.id.startsWith('_'))
8
- const sortedPosts = filteredPosts.sort(
9
- (a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) =>
10
- b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
11
- )
7
+ const sortedPosts = await getSortedFilteredPosts()
8
9
const rss = `<?xml version="1.0" encoding="UTF-8" ?>
10
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
@@ -43,12 +39,7 @@ export async function generateRSS(context: APIContext) {
39
}
40
41
export async function generateAtom(context: APIContext) {
46
- const posts = await getCollection('posts')
47
- const filteredPosts = posts.filter((post) => !post.id.startsWith('_'))
48
- const sortedPosts = filteredPosts.sort(
49
- (a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) =>
50
- b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
51
- )
42
+ const sortedPosts = await getSortedFilteredPosts()
43
44
const atom = `<?xml version="1.0" encoding="utf-8"?>
45
<feed xmlns="http://www.w3.org/2005/Atom">