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 - &copy;
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 + &copy;
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">