init with dummy posts

This commit is contained in:
2025-03-18 15:07:45 -05:00
commit 1120a7720a
42 changed files with 9902 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# IDE
.idea/
.vscode/
+113
View File
@@ -0,0 +1,113 @@
# Confessions of Grace
A blog about Reformed theology built with Next.js and TypeScript, featuring Markdown-based content management.
## Features
- Modern static site built with Next.js
- TypeScript for type safety
- Content management using Markdown files
- Responsive design with Tailwind CSS
- Flat design style with Reformed theology aesthetics
## Getting Started
### Prerequisites
- Node.js (>= 16.x)
- npm or yarn
### Installation
1. Clone the repository
```bash
git clone https://github.com/yourusername/confessions-of-grace.git
cd confessions-of-grace
```
2. Install dependencies
```bash
npm install
# or
yarn install
```
3. Run the development server
```bash
npm run dev
# or
yarn dev
```
4. Open [http://localhost:3000](http://localhost:3000) in your browser to see the result.
## Project Structure
```
├── public/ # Static assets
│ └── images/ # Images for blog posts
├── src/ # Source code
│ ├── components/ # React components
│ ├── lib/ # Utility functions
│ ├── pages/ # Next.js pages
│ ├── posts/ # Markdown blog posts
│ ├── styles/ # CSS styles
│ └── types/ # TypeScript type definitions
├── .gitignore
├── next.config.js
├── package.json
├── postcss.config.js
├── README.md
├── tailwind.config.js
└── tsconfig.json
```
## Writing Blog Posts
To add a new blog post, create a new Markdown file in the `src/posts` directory with the following format:
```markdown
---
title: "Your Post Title"
date: "YYYY-MM-DD"
author: "Your Name"
excerpt: "A brief excerpt of your post"
tags: ["Tag1", "Tag2"]
coverImage: "/images/your-image.jpg"
---
# Your Post Title
Your content goes here...
```
## Deployment
The site can be deployed to various platforms:
### Vercel (Recommended)
```bash
npm install -g vercel
vercel
```
### Standard Build
```bash
npm run build
npm start
```
## Customization
- Modify the design in `tailwind.config.js` and `src/styles/globals.css`
- Update site content in component files
- Add or modify pages in the `src/pages` directory
## License
This project is licensed under the MIT License.
+156
View File
@@ -0,0 +1,156 @@
[debug] [2025-03-18T20:05:33.847Z] ----------------------------------------------------------------------
[debug] [2025-03-18T20:05:33.849Z] Command: C:\Program Files\nodejs\node.exe C:\Users\Austin\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js init
[debug] [2025-03-18T20:05:33.849Z] CLI Version: 13.34.0
[debug] [2025-03-18T20:05:33.849Z] Platform: win32
[debug] [2025-03-18T20:05:33.850Z] Node Version: v22.14.0
[debug] [2025-03-18T20:05:33.850Z] Time: Tue Mar 18 2025 15:05:33 GMT-0500 (Central Daylight Time)
[debug] [2025-03-18T20:05:33.850Z] ----------------------------------------------------------------------
[debug]
[debug] [2025-03-18T20:05:33.853Z] >>> [apiv2][query] GET https://firebase-public.firebaseio.com/cli.json [none]
[debug] [2025-03-18T20:05:33.882Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2025-03-18T20:05:33.882Z] > authorizing via signed-in user ([email protected])
[info]
######## #### ######## ######## ######## ### ###### ########
## ## ## ## ## ## ## ## ## ## ##
###### ## ######## ###### ######## ######### ###### ######
## ## ## ## ## ## ## ## ## ## ##
## #### ## ## ######## ######## ## ## ###### ########
You're about to initialize a Firebase project in this directory:
C:\Users\Austin\Documents\confessions-of-grace
[debug] [2025-03-18T20:05:34.924Z] <<< [apiv2][status] GET https://firebase-public.firebaseio.com/cli.json 200
[debug] [2025-03-18T20:05:34.924Z] <<< [apiv2][body] GET https://firebase-public.firebaseio.com/cli.json {"cloudBuildErrorAfter":1594252800000,"cloudBuildWarnAfter":1590019200000,"defaultNode10After":1594252800000,"minVersion":"3.0.5","node8DeploysDisabledAfter":1613390400000,"node8RuntimeDisabledAfter":1615809600000,"node8WarnAfter":1600128000000}
[info]
=== Project Setup
[info]
[info] First, let's associate this project directory with a Firebase project.
[info] You can create multiple project aliases by running firebase use --add,
[info] but for now we'll just set up a default project.
[info]
[debug] [2025-03-18T20:05:43.813Z] Checked if tokens are valid: false, expires at: 1742058678460
[debug] [2025-03-18T20:05:43.813Z] Checked if tokens are valid: false, expires at: 1742058678460
[debug] [2025-03-18T20:05:43.813Z] > refreshing access token with scopes: []
[debug] [2025-03-18T20:05:43.814Z] >>> [apiv2][query] POST https://www.googleapis.com/oauth2/v3/token [none]
[debug] [2025-03-18T20:05:43.814Z] >>> [apiv2][body] POST https://www.googleapis.com/oauth2/v3/token [omitted]
[debug] [2025-03-18T20:05:43.974Z] <<< [apiv2][status] POST https://www.googleapis.com/oauth2/v3/token 200
[debug] [2025-03-18T20:05:43.974Z] <<< [apiv2][body] POST https://www.googleapis.com/oauth2/v3/token [omitted]
[debug] [2025-03-18T20:05:43.980Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects pageSize=100
[debug] [2025-03-18T20:05:44.282Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects 200
[debug] [2025-03-18T20:05:44.282Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects [omitted]
[info] i Using project confessions-of-grace (confessions-of-grace)
[info]
=== Hosting Setup
[debug] [2025-03-18T20:05:45.777Z] Checked if tokens are valid: true, expires at: 1742331942974
[debug] [2025-03-18T20:05:45.777Z] Checked if tokens are valid: true, expires at: 1742331942974
[debug] [2025-03-18T20:05:45.777Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/confessions-of-grace [none]
[debug] [2025-03-18T20:05:45.895Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/confessions-of-grace 200
[debug] [2025-03-18T20:05:45.895Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/confessions-of-grace {"projectId":"confessions-of-grace","projectNumber":"545148895351","displayName":"confessions-of-grace","name":"projects/confessions-of-grace","resources":{"hostingSite":"confessions-of-grace"},"state":"ACTIVE","etag":"1_8cba48cb-7559-4859-82ed-0459928e3a23"}
[info]
[info] i Didn't detect a .git folder. Assuming C:\Users\Austin\Documents\confessions-of-grace is the project root.
[info] i Authorizing with GitHub to upload your service account to a GitHub repository's secrets store.
[info]
[info] Visit this URL on this device to log in:
[info] https://github.com/login/oauth/authorize?client_id=89cf50f02ac6aaed3484&state=50711215&redirect_uri=http%3A%2F%2Flocalhost%3A9005&scope=read%3Auser%20repo%20public_repo
[info]
[info] Waiting for authentication...
[debug] [2025-03-18T20:06:26.585Z] >>> [apiv2][query] POST https://github.com/login/oauth/access_token [none]
[debug] [2025-03-18T20:06:26.585Z] >>> [apiv2][body] POST https://github.com/login/oauth/access_token [stream]
[debug] [2025-03-18T20:06:26.815Z] <<< [apiv2][status] POST https://github.com/login/oauth/access_token 200
[debug] [2025-03-18T20:06:26.815Z] <<< [apiv2][body] POST https://github.com/login/oauth/access_token {"access_token":"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","token_type":"bearer","scope":"read:user,repo"}
[debug] [2025-03-18T20:06:26.818Z] >>> [apiv2][query] GET https://api.github.com/user [none]
[debug] [2025-03-18T20:06:27.119Z] <<< [apiv2][status] GET https://api.github.com/user 200
[debug] [2025-03-18T20:06:27.119Z] <<< [apiv2][body] GET https://api.github.com/user {"login":"auggie2lbcf","id":157770486,"node_id":"U_kgDOCWdi9g","avatar_url":"https://avatars.githubusercontent.com/u/157770486?v=4","gravatar_id":"","url":"https://api.github.com/users/auggie2lbcf","html_url":"https://github.com/auggie2lbcf","followers_url":"https://api.github.com/users/auggie2lbcf/followers","following_url":"https://api.github.com/users/auggie2lbcf/following{/other_user}","gists_url":"https://api.github.com/users/auggie2lbcf/gists{/gist_id}","starred_url":"https://api.github.com/users/auggie2lbcf/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/auggie2lbcf/subscriptions","organizations_url":"https://api.github.com/users/auggie2lbcf/orgs","repos_url":"https://api.github.com/users/auggie2lbcf/repos","events_url":"https://api.github.com/users/auggie2lbcf/events{/privacy}","received_events_url":"https://api.github.com/users/auggie2lbcf/received_events","type":"User","user_view_type":"private","site_admin":false,"name":"Auggie","company":null,"blog":"thebennett.net","location":null,"email":null,"hireable":true,"bio":null,"twitter_username":"auggie2lbcf","notification_email":null,"public_repos":17,"public_gists":0,"followers":0,"following":3,"created_at":"2024-01-26T01:41:43Z","updated_at":"2025-03-18T19:22:54Z","private_gists":0,"total_private_repos":4,"owned_private_repos":4,"disk_usage":242961,"collaborators":0,"two_factor_authentication":true,"plan":{"name":"free","space":976562499,"collaborators":0,"private_repos":10000}}
[info]
[info] + Success! Logged into GitHub as auggie2lbcf
[info]
[debug] [2025-03-18T20:06:37.568Z] >>> [apiv2][query] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key type=owner
[debug] [2025-03-18T20:06:37.719Z] <<< [apiv2][status] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key 404
[debug] [2025-03-18T20:06:37.719Z] <<< [apiv2][body] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key {"message":"Not Found","documentation_url":"https://docs.github.com/rest/actions/secrets#get-a-repository-public-key","status":"404"}
[info]
[info]
[error] ! The provided authorization cannot be used with this repository. If this repository is in an organization, did you remember to grant access?
[info]
[info] i Action required: Visit this URL to ensure access has been granted to the appropriate organization(s) for the Firebase CLI GitHub OAuth App:
[info] https://github.com/settings/connections/applications/89cf50f02ac6aaed3484
[info]
[debug] [2025-03-18T20:06:48.785Z] >>> [apiv2][query] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key type=owner
[debug] [2025-03-18T20:06:48.942Z] <<< [apiv2][status] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key 404
[debug] [2025-03-18T20:06:48.943Z] <<< [apiv2][body] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key {"message":"Not Found","documentation_url":"https://docs.github.com/rest/actions/secrets#get-a-repository-public-key","status":"404"}
[info]
[info]
[error] ! The provided authorization cannot be used with this repository. If this repository is in an organization, did you remember to grant access?
[info]
[info] i Action required: Visit this URL to ensure access has been granted to the appropriate organization(s) for the Firebase CLI GitHub OAuth App:
[info] https://github.com/settings/connections/applications/89cf50f02ac6aaed3484
[info]
[debug] [2025-03-18T20:06:56.039Z] ----------------------------------------------------------------------
[debug] [2025-03-18T20:06:56.041Z] Command: C:\Program Files\nodejs\node.exe C:\Users\Austin\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js init
[debug] [2025-03-18T20:06:56.041Z] CLI Version: 13.34.0
[debug] [2025-03-18T20:06:56.041Z] Platform: win32
[debug] [2025-03-18T20:06:56.041Z] Node Version: v22.14.0
[debug] [2025-03-18T20:06:56.041Z] Time: Tue Mar 18 2025 15:06:56 GMT-0500 (Central Daylight Time)
[debug] [2025-03-18T20:06:56.041Z] ----------------------------------------------------------------------
[debug]
[debug] [2025-03-18T20:06:56.045Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2025-03-18T20:06:56.046Z] > authorizing via signed-in user ([email protected])
[info]
######## #### ######## ######## ######## ### ###### ########
## ## ## ## ## ## ## ## ## ## ##
###### ## ######## ###### ######## ######### ###### ######
## ## ## ## ## ## ## ## ## ## ##
## #### ## ## ######## ######## ## ## ###### ########
You're about to initialize a Firebase project in this directory:
C:\Users\Austin\Documents\confessions-of-grace
[info]
=== Project Setup
[info]
[info] First, let's associate this project directory with a Firebase project.
[info] You can create multiple project aliases by running firebase use --add,
[info] but for now we'll just set up a default project.
[info]
[debug] [2025-03-18T20:07:01.993Z] Checked if tokens are valid: true, expires at: 1742331942974
[debug] [2025-03-18T20:07:01.993Z] Checked if tokens are valid: true, expires at: 1742331942974
[debug] [2025-03-18T20:07:01.994Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects pageSize=100
[debug] [2025-03-18T20:07:02.241Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects 200
[debug] [2025-03-18T20:07:02.242Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects [omitted]
[info] i Using project confessions-of-grace (confessions-of-grace)
[info]
=== Hosting Setup
[debug] [2025-03-18T20:07:03.837Z] Checked if tokens are valid: true, expires at: 1742331942974
[debug] [2025-03-18T20:07:03.837Z] Checked if tokens are valid: true, expires at: 1742331942974
[debug] [2025-03-18T20:07:03.837Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/confessions-of-grace [none]
[debug] [2025-03-18T20:07:03.957Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/confessions-of-grace 200
[debug] [2025-03-18T20:07:03.957Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/confessions-of-grace {"projectId":"confessions-of-grace","projectNumber":"545148895351","displayName":"confessions-of-grace","name":"projects/confessions-of-grace","resources":{"hostingSite":"confessions-of-grace"},"state":"ACTIVE","etag":"1_8cba48cb-7559-4859-82ed-0459928e3a23"}
[info]
[info] i Didn't detect a .git folder. Assuming C:\Users\Austin\Documents\confessions-of-grace is the project root.
[info] i Authorizing with GitHub to upload your service account to a GitHub repository's secrets store.
[info]
[info] Visit this URL on this device to log in:
[info] https://github.com/login/oauth/authorize?client_id=89cf50f02ac6aaed3484&state=533693003&redirect_uri=http%3A%2F%2Flocalhost%3A9005&scope=read%3Auser%20repo%20public_repo
[info]
[info] Waiting for authentication...
[debug] [2025-03-18T20:07:16.492Z] >>> [apiv2][query] POST https://github.com/login/oauth/access_token [none]
[debug] [2025-03-18T20:07:16.492Z] >>> [apiv2][body] POST https://github.com/login/oauth/access_token [stream]
[debug] [2025-03-18T20:07:16.689Z] <<< [apiv2][status] POST https://github.com/login/oauth/access_token 200
[debug] [2025-03-18T20:07:16.689Z] <<< [apiv2][body] POST https://github.com/login/oauth/access_token {"access_token":"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","token_type":"bearer","scope":"read:user,repo"}
[debug] [2025-03-18T20:07:16.692Z] >>> [apiv2][query] GET https://api.github.com/user [none]
[debug] [2025-03-18T20:07:16.976Z] <<< [apiv2][status] GET https://api.github.com/user 200
[debug] [2025-03-18T20:07:16.976Z] <<< [apiv2][body] GET https://api.github.com/user {"login":"auggie2lbcf","id":157770486,"node_id":"U_kgDOCWdi9g","avatar_url":"https://avatars.githubusercontent.com/u/157770486?v=4","gravatar_id":"","url":"https://api.github.com/users/auggie2lbcf","html_url":"https://github.com/auggie2lbcf","followers_url":"https://api.github.com/users/auggie2lbcf/followers","following_url":"https://api.github.com/users/auggie2lbcf/following{/other_user}","gists_url":"https://api.github.com/users/auggie2lbcf/gists{/gist_id}","starred_url":"https://api.github.com/users/auggie2lbcf/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/auggie2lbcf/subscriptions","organizations_url":"https://api.github.com/users/auggie2lbcf/orgs","repos_url":"https://api.github.com/users/auggie2lbcf/repos","events_url":"https://api.github.com/users/auggie2lbcf/events{/privacy}","received_events_url":"https://api.github.com/users/auggie2lbcf/received_events","type":"User","user_view_type":"private","site_admin":false,"name":"Auggie","company":null,"blog":"thebennett.net","location":null,"email":null,"hireable":true,"bio":null,"twitter_username":"auggie2lbcf","notification_email":null,"public_repos":17,"public_gists":0,"followers":0,"following":3,"created_at":"2024-01-26T01:41:43Z","updated_at":"2025-03-18T19:22:54Z","private_gists":0,"total_private_repos":4,"owned_private_repos":4,"disk_usage":242961,"collaborators":0,"two_factor_authentication":true,"plan":{"name":"free","space":976562499,"collaborators":0,"private_repos":10000}}
[info]
[info] + Success! Logged into GitHub as auggie2lbcf
[info]
[debug] [2025-03-18T20:07:29.570Z] >>> [apiv2][query] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key type=owner
[debug] [2025-03-18T20:07:29.729Z] <<< [apiv2][status] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key 404
[debug] [2025-03-18T20:07:29.730Z] <<< [apiv2][body] GET https://api.github.com/repos/auggie2lbcf/confessionofgrace/actions/secrets/public-key {"message":"Not Found","documentation_url":"https://docs.github.com/rest/actions/secrets#get-a-repository-public-key","status":"404"}
[info]
[info]
[error] ! The provided authorization cannot be used with this repository. If this repository is in an organization, did you remember to grant access?
[info]
[info] i Action required: Visit this URL to ensure access has been granted to the appropriate organization(s) for the Firebase CLI GitHub OAuth App:
[info] https://github.com/settings/connections/applications/89cf50f02ac6aaed3484
[info]
+7
View File
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig
+6931
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "confessions-of-grace",
"version": "1.0.0",
"description": "A blog about reformed theology",
"main": "index.js",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit"
},
"keywords": [
"blog",
"reformed",
"theology",
"grace"
],
"author": "",
"license": "MIT",
"dependencies": {
"autoprefixer": "^10.4.17",
"date-fns": "^3.3.1",
"gray-matter": "^4.0.3",
"next": "^14.1.0",
"postcss": "^8.4.33",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"tailwindcss": "^3.4.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^20.11.7",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"eslint": "^8.56.0",
"eslint-config-next": "^14.1.0",
"typescript": "^5.3.3"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 675">
<!-- Background -->
<rect width="1200" height="675" fill="#f8f9fa" />
<!-- Decorative border -->
<rect x="20" y="20" width="1160" height="635" fill="none" stroke="#9d8c70" stroke-width="4" />
<!-- Open Bible -->
<path d="M350,200 L600,150 L850,200 L850,400 L600,450 L350,400 Z" fill="#e2d8c6" />
<path d="M600,150 L600,450" stroke="#9d8c70" stroke-width="3" />
<!-- Bible pages texture - left side -->
<path d="M360,205 L590,160 L590,440 L360,395 Z" fill="#f8f9fa" />
<line x1="380" y1="210" x2="570" y2="175" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="230" x2="570" y2="195" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="250" x2="570" y2="215" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="270" x2="570" y2="235" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="290" x2="570" y2="255" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="310" x2="570" y2="275" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="330" x2="570" y2="295" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="350" x2="570" y2="315" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="370" x2="570" y2="335" stroke="#d1c5ad" stroke-width="1" />
<!-- Bible pages texture - right side -->
<path d="M610,160 L840,205 L840,395 L610,440 Z" fill="#f8f9fa" />
<line x1="630" y1="175" x2="820" y2="210" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="195" x2="820" y2="230" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="215" x2="820" y2="250" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="235" x2="820" y2="270" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="255" x2="820" y2="290" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="275" x2="820" y2="310" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="295" x2="820" y2="330" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="315" x2="820" y2="350" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="335" x2="820" y2="370" stroke="#d1c5ad" stroke-width="1" />
<!-- Romans 3:28 on Bible page -->
<text x="470" y="280" font-family="Baskerville, Georgia, serif" font-size="18" fill="#695c4a" text-anchor="middle" transform="rotate(-5, 470, 280)">Romans 3:28</text>
<!-- Title background -->
<rect x="200" y="500" width="800" height="100" fill="#9d8c70" />
<!-- Title text -->
<text x="600" y="565" font-family="Baskerville, Georgia, serif" font-size="48" text-anchor="middle" fill="#fff">SOLA FIDE</text>
<text x="600" y="595" font-family="Helvetica Neue, Arial, sans-serif" font-size="24" text-anchor="middle" fill="#f8f9fa">Justification by Faith Alone</text>
<!-- Scripture reference -->
<text x="600" y="470" font-family="Baskerville, Georgia, serif" font-size="20" text-anchor="middle" fill="#695c4a" font-style="italic">"One is justified by faith apart from works of the law."</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 675">
<!-- Background -->
<rect width="1200" height="675" fill="#f8f9fa" />
<!-- Decorative border -->
<rect x="20" y="20" width="1160" height="635" fill="none" stroke="#9d8c70" stroke-width="4" />
<!-- Cross -->
<rect x="550" y="100" width="100" height="350" fill="#695c4a" />
<rect x="425" y="200" width="350" height="100" fill="#695c4a" />
<!-- Light rays -->
<polygon points="600,100 500,50 600,50" fill="#e2d8c6" opacity="0.5" />
<polygon points="600,100 700,50 600,50" fill="#e2d8c6" opacity="0.5" />
<polygon points="600,100 800,100 700,50" fill="#e2d8c6" opacity="0.5" />
<polygon points="600,100 400,100 500,50" fill="#e2d8c6" opacity="0.5" />
<!-- Title background -->
<rect x="200" y="500" width="800" height="100" fill="#9d8c70" />
<!-- Title text -->
<text x="600" y="565" font-family="Baskerville, Georgia, serif" font-size="48" text-anchor="middle" fill="#fff">SOLA GRATIA</text>
<text x="600" y="595" font-family="Helvetica Neue, Arial, sans-serif" font-size="24" text-anchor="middle" fill="#f8f9fa">Salvation by Grace Alone</text>
<!-- Ephesians 2:8 reference -->
<text x="600" y="470" font-family="Baskerville, Georgia, serif" font-size="20" text-anchor="middle" fill="#695c4a" font-style="italic">"For by grace you have been saved through faith..."</text>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 675">
<!-- Background -->
<rect width="1200" height="675" fill="#f8f9fa" />
<!-- Decorative border -->
<rect x="20" y="20" width="1160" height="635" fill="none" stroke="#9d8c70" stroke-width="4" />
<!-- Open Bible -->
<path d="M350,200 L600,150 L850,200 L850,400 L600,450 L350,400 Z" fill="#e2d8c6" />
<path d="M600,150 L600,450" stroke="#9d8c70" stroke-width="3" />
<!-- Bible pages texture - left side -->
<path d="M360,205 L590,160 L590,440 L360,395 Z" fill="#f8f9fa" />
<line x1="380" y1="210" x2="570" y2="175" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="230" x2="570" y2="195" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="250" x2="570" y2="215" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="270" x2="570" y2="235" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="290" x2="570" y2="255" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="310" x2="570" y2="275" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="330" x2="570" y2="295" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="350" x2="570" y2="315" stroke="#d1c5ad" stroke-width="1" />
<line x1="380" y1="370" x2="570" y2="335" stroke="#d1c5ad" stroke-width="1" />
<!-- Bible pages texture - right side -->
<path d="M610,160 L840,205 L840,395 L610,440 Z" fill="#f8f9fa" />
<line x1="630" y1="175" x2="820" y2="210" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="195" x2="820" y2="230" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="215" x2="820" y2="250" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="235" x2="820" y2="270" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="255" x2="820" y2="290" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="275" x2="820" y2="310" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="295" x2="820" y2="330" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="315" x2="820" y2="350" stroke="#d1c5ad" stroke-width="1" />
<line x1="630" y1="335" x2="820" y2="370" stroke="#d1c5ad" stroke-width="1" />
<!-- Romans 3:28 on Bible page -->
<text x="470" y="280" font-family="Baskerville, Georgia, serif" font-size="18" fill="#695c4a" text-anchor="middle" transform="rotate(-5, 470, 280)">Romans 3:28</text>
<!-- Title background -->
<rect x="200" y="500" width="800" height="100" fill="#9d8c70" />
<!-- Title text -->
<text x="600" y="565" font-family="Baskerville, Georgia, serif" font-size="48" text-anchor="middle" fill="#fff">SOLA FIDE</text>
<text x="600" y="595" font-family="Helvetica Neue, Arial, sans-serif" font-size="24" text-anchor="middle" fill="#f8f9fa">Justification by Faith Alone</text>
<!-- Scripture reference -->
<text x="600" y="470" font-family="Baskerville, Georgia, serif" font-size="20" text-anchor="middle" fill="#695c4a" font-style="italic">"One is justified by faith apart from works of the law."</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+163
View File
@@ -0,0 +1,163 @@
import React, { useState } from 'react';
interface CommentFormProps {
postId: string;
}
const CommentSection: React.FC<CommentFormProps> = ({ postId }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [comment, setComment] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Basic validation
if (!name.trim() || !email.trim() || !comment.trim()) {
setError('All fields are required');
return;
}
setIsSubmitting(true);
setError(null);
try {
// In a real implementation, you would send this data to your API
// For now, we'll just simulate a successful submission
await new Promise(resolve => setTimeout(resolve, 1000));
// Clear form and show success message
setName('');
setEmail('');
setComment('');
setIsSubmitted(true);
setIsSubmitting(false);
// Reset success message after 5 seconds
setTimeout(() => {
setIsSubmitted(false);
}, 5000);
} catch (err) {
setError('Something went wrong. Please try again later.');
setIsSubmitting(false);
}
};
return (
<div className="mt-12 pt-6 border-t border-primary-200">
<h3 className="text-2xl font-bold mb-6">Leave a Comment</h3>
{isSubmitted && (
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md mb-6">
Your comment has been submitted and is awaiting moderation. Thank you!
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md mb-6">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="name" className="block text-primary-700 mb-1">
Name <span className="text-red-500">*</span>
</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-primary-700 mb-1">
Email <span className="text-red-500">*</span>
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
/>
</div>
</div>
<div>
<label htmlFor="comment" className="block text-primary-700 mb-1">
Comment <span className="text-red-500">*</span>
</label>
<textarea
id="comment"
rows={6}
value={comment}
onChange={(e) => setComment(e.target.value)}
className="w-full px-4 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
></textarea>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="save-info"
className="h-4 w-4 text-accent border-primary-300 rounded focus:ring-accent"
/>
<label htmlFor="save-info" className="ml-2 block text-sm text-primary-600">
Save my name and email for the next time I comment
</label>
</div>
<button
type="submit"
className="button"
disabled={isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Post Comment'}
</button>
</form>
{/* Sample comments - in a real implementation, these would be fetched from an API */}
<div className="mt-12">
<h3 className="text-xl font-bold mb-6">Comments (2)</h3>
<div className="space-y-6">
<div className="bg-white p-6 rounded-md shadow-sm border border-primary-200">
<div className="flex justify-between items-start mb-4">
<div>
<h4 className="font-bold">John Calvin</h4>
<p className="text-sm text-primary-500">March 10, 2025</p>
</div>
</div>
<p className="text-primary-700">
A profound meditation on the doctrine of grace. I particularly appreciate your emphasis on how this truth transforms our daily lives, not just our theological understanding.
</p>
</div>
<div className="bg-white p-6 rounded-md shadow-sm border border-primary-200">
<div className="flex justify-between items-start mb-4">
<div>
<h4 className="font-bold">Martin Luther</h4>
<p className="text-sm text-primary-500">March 5, 2025</p>
</div>
</div>
<p className="text-primary-700">
This post eloquently articulates what I've been trying to explain to my congregation. The way you connected Scripture with practical application was masterful. I'll be sharing this widely.
</p>
</div>
</div>
</div>
</div>
);
};
export default CommentSection;
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import { format, parseISO } from 'date-fns';
interface DateFormatterProps {
dateString: string;
formatString?: string;
className?: string;
}
const DateFormatter: React.FC<DateFormatterProps> = ({
dateString,
formatString = 'MMMM d, yyyy',
className = ''
}) => {
const date = parseISO(dateString);
return (
<time dateTime={dateString} className={className}>
{format(date, formatString)}
</time>
);
};
export default DateFormatter;
+70
View File
@@ -0,0 +1,70 @@
import React from 'react';
import Link from 'next/link';
const Footer: React.FC = () => {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-primary-800 text-white mt-16">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 className="text-xl font-bold mb-4 text-white">Confessions of Grace</h3>
<p className="text-primary-300">
A blog dedicated to exploring the doctrines of grace and Reformed theology.
</p>
</div>
<div>
<h3 className="text-xl font-bold mb-4 text-white">Navigation</h3>
<ul className="space-y-2">
<li>
<Link href="/" className="text-primary-300 hover:text-white">
Home
</Link>
</li>
<li>
<Link href="/about" className="text-primary-300 hover:text-white">
About
</Link>
</li>
<li>
<Link href="/archive" className="text-primary-300 hover:text-white">
Archive
</Link>
</li>
<li>
<Link href="/resources" className="text-primary-300 hover:text-white">
Resources
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-xl font-bold mb-4 text-white">Subscribe</h3>
<p className="text-primary-300 mb-4">
Stay updated with the latest posts.
</p>
<div className="flex">
<input
type="email"
placeholder="Your email"
className="px-4 py-2 w-full rounded-l-md bg-primary-700 border border-primary-600 text-white placeholder-primary-400"
/>
<button className="bg-accent hover:bg-accent-dark px-4 py-2 rounded-r-md">
Subscribe
</button>
</div>
</div>
</div>
<div className="border-t border-primary-700 mt-8 pt-8 text-center text-primary-400">
<p>&copy; {currentYear} Confessions of Grace. All rights reserved.</p>
<p className="mt-2 text-sm">
"For by grace you have been saved through faith. And this is not your own doing; it is the gift of God." Ephesians 2:8
</p>
</div>
</div>
</footer>
);
};
export default Footer;
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import Link from 'next/link';
const Header: React.FC = () => {
return (
<header className="bg-white shadow-sm border-b border-primary-200">
<div className="container mx-auto px-4 py-6">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<Link href="/" className="no-underline">
<h1 className="text-3xl font-bold text-primary-900 mb-0">Confessions of Grace</h1>
</Link>
<p className="text-primary-500 italic text-sm">Exploring Reformed Theology</p>
</div>
<nav className="flex space-x-6">
<Link href="/" className="text-primary-700 hover:text-accent-dark">
Home
</Link>
<Link href="/about" className="text-primary-700 hover:text-accent-dark">
About
</Link>
<Link href="/archive" className="text-primary-700 hover:text-accent-dark">
Archive
</Link>
<Link href="/resources" className="text-primary-700 hover:text-accent-dark">
Resources
</Link>
</nav>
</div>
</div>
</header>
);
};
export default Header;
+36
View File
@@ -0,0 +1,36 @@
import React, { ReactNode } from 'react';
import Head from 'next/head';
import Header from './Header';
import Footer from './Footer';
interface LayoutProps {
children: ReactNode;
title?: string;
description?: string;
}
const Layout: React.FC<LayoutProps> = ({
children,
title = 'Confessions of Grace',
description = 'A blog dedicated to exploring the doctrines of grace and Reformed theology.'
}) => {
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
{children}
</main>
<Footer />
</div>
</>
);
};
export default Layout;
+50
View File
@@ -0,0 +1,50 @@
import React from 'react';
import Head from 'next/head';
interface MetaProps {
title?: string;
description?: string;
keywords?: string;
image?: string;
url?: string;
type?: string;
}
const Meta: React.FC<MetaProps> = ({
title = 'Confessions of Grace',
description = 'A blog dedicated to exploring the doctrines of grace and Reformed theology.',
keywords = 'reformed theology, doctrines of grace, christianity, calvinism, theology',
image = '/images/og-default.jpg',
url = 'https://confessionsofgrace.com',
type = 'website'
}) => {
const siteTitle = title === 'Confessions of Grace'
? 'Confessions of Grace | Reformed Theology Blog'
: `${title} | Confessions of Grace`;
return (
<Head>
<title>{siteTitle}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{/* Open Graph / Facebook */}
<meta property="og:type" content={type} />
<meta property="og:url" content={url} />
<meta property="og:title" content={siteTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
{/* Twitter */}
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={url} />
<meta property="twitter:title" content={siteTitle} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={image} />
</Head>
);
};
export default Meta;
+113
View File
@@ -0,0 +1,113 @@
import React from 'react';
import Link from 'next/link';
interface PaginationProps {
currentPage: number;
totalPages: number;
basePath: string;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
basePath
}) => {
// Generate page numbers to display
const getPageNumbers = () => {
// Always show first and last page
// Show 2 pages before and after current page
const pageNumbers = new Set<number>();
// Add current page
pageNumbers.add(currentPage);
// Add adjacent pages
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
pageNumbers.add(i);
}
// Always add first and last page
pageNumbers.add(1);
pageNumbers.add(totalPages);
// Convert to array and sort
return Array.from(pageNumbers).sort((a, b) => a - b);
};
const pageNumbers = getPageNumbers();
// Generate link for a page number
const getPageLink = (pageNum: number) => {
if (pageNum === 1) {
return basePath;
}
return `${basePath}/page/${pageNum}`;
};
return (
<nav className="flex justify-center my-8">
<ul className="flex items-center space-x-1">
{/* Previous page button */}
{currentPage > 1 && (
<li>
<Link
href={getPageLink(currentPage - 1)}
className="flex items-center justify-center w-10 h-10 rounded-md border border-primary-200 bg-white text-primary-700 hover:bg-primary-50"
aria-label="Previous page"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
</li>
)}
{/* Page numbers */}
{pageNumbers.map((pageNum, index) => {
// Add ellipsis if there's a gap
const showEllipsisBefore = index > 0 && pageNum > pageNumbers[index - 1] + 1;
return (
<React.Fragment key={pageNum}>
{showEllipsisBefore && (
<li className="flex items-center justify-center w-10 h-10 text-primary-500">
...
</li>
)}
<li>
<Link
href={getPageLink(pageNum)}
className={`flex items-center justify-center w-10 h-10 rounded-md border ${
pageNum === currentPage
? 'bg-accent text-white border-accent font-bold'
: 'bg-white text-primary-700 border-primary-200 hover:bg-primary-50'
}`}
aria-current={pageNum === currentPage ? 'page' : undefined}
>
{pageNum}
</Link>
</li>
</React.Fragment>
);
})}
{/* Next page button */}
{currentPage < totalPages && (
<li>
<Link
href={getPageLink(currentPage + 1)}
className="flex items-center justify-center w-10 h-10 rounded-md border border-primary-200 bg-white text-primary-700 hover:bg-primary-50"
aria-label="Next page"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
</li>
)}
</ul>
</nav>
);
};
export default Pagination;
+69
View File
@@ -0,0 +1,69 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { format } from 'date-fns';
import { PostMetadata } from '@/types';
interface PostCardProps {
post: PostMetadata;
}
const PostCard: React.FC<PostCardProps> = ({ post }) => {
return (
<article className="card hover:shadow-md transition-shadow duration-200">
{post.coverImage && (
<div className="mb-4 relative h-48 w-full overflow-hidden rounded-md">
<Image
src={post.coverImage}
alt={post.title}
fill
className="object-cover"
/>
</div>
)}
<h2 className="text-xl font-bold mb-2">
<Link href={`/posts/${post.id}`} className="hover:text-accent-dark">
{post.title}
</Link>
</h2>
<div className="flex items-center mb-2 text-sm text-primary-500">
<span>{post.author}</span>
<span className="mx-2"></span>
<time dateTime={post.date}>
{format(new Date(post.date), 'MMMM d, yyyy')}
</time>
</div>
<p className="text-primary-600 mb-4">{post.excerpt}</p>
<div className="flex flex-wrap gap-2 mb-4">
{post.tags.map(tag => (
<Link
key={tag}
href={`/tags/${tag}`}
className="text-xs bg-primary-100 text-primary-600 px-2 py-1 rounded-md hover:bg-primary-200"
>
{tag}
</Link>
))}
</div>
<Link href={`/posts/${post.id}`} className="inline-flex items-center text-accent-dark hover:text-accent">
Read more
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 ml-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
</Link>
</article>
);
};
export default PostCard;
+48
View File
@@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { useRouter } from 'next/router';
const SearchBar: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
}
};
return (
<form onSubmit={handleSubmit} className="relative">
<input
type="text"
placeholder="Search posts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 pl-10 pr-4 rounded-md border border-primary-200 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
<button
type="submit"
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-primary-400"
aria-label="Search"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</button>
</form>
);
};
export default SearchBar;
+118
View File
@@ -0,0 +1,118 @@
import React from 'react';
interface ShareButtonsProps {
url: string;
title: string;
description?: string;
}
const ShareButtons: React.FC<ShareButtonsProps> = ({ url, title, description = '' }) => {
// Encode parameters for sharing URLs
const encodedUrl = encodeURIComponent(url);
const encodedTitle = encodeURIComponent(title);
const encodedDescription = encodeURIComponent(description);
// Generate sharing URLs
const facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`;
const twitterUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
const linkedinUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;
const emailUrl = `mailto:?subject=${encodedTitle}&body=${encodedDescription}%0A%0A${encodedUrl}`;
return (
<div className="flex items-center space-x-3">
<span className="text-primary-600 font-medium">Share:</span>
{/* Facebook */}
<a
href={facebookUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-accent-dark transition-colors duration-200"
aria-label="Share on Facebook"
title="Share on Facebook"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
clipRule="evenodd"
/>
</svg>
</a>
{/* Twitter */}
<a
href={twitterUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-accent-dark transition-colors duration-200"
aria-label="Share on Twitter"
title="Share on Twitter"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"
/>
</svg>
</a>
{/* LinkedIn */}
<a
href={linkedinUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-accent-dark transition-colors duration-200"
aria-label="Share on LinkedIn"
title="Share on LinkedIn"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"
clipRule="evenodd"
/>
</svg>
</a>
{/* Email */}
<a
href={emailUrl}
className="text-primary-500 hover:text-accent-dark transition-colors duration-200"
aria-label="Share via Email"
title="Share via Email"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</a>
</div>
);
};
export default ShareButtons;
+96
View File
@@ -0,0 +1,96 @@
import React from 'react';
import Link from 'next/link';
import TagList from './TagList';
interface SidebarProps {
recentPosts: Array<{
id: string;
title: string;
date: string;
}>;
tags?: Array<{
tag: string;
count: number;
}>;
}
const Sidebar: React.FC<SidebarProps> = ({ recentPosts, tags = [] }) => {
return (
<aside className="space-y-8">
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">About</h2>
<p className="text-primary-700 mb-4">
Confessions of Grace explores the riches of Reformed theology and the doctrines of grace.
</p>
<Link
href="/about"
className="text-accent-dark hover:text-accent inline-flex items-center"
>
Learn more
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 ml-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</Link>
</div>
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">Recent Posts</h2>
<ul className="space-y-3">
{recentPosts.map(post => (
<li key={post.id}>
<Link
href={`/posts/${post.id}`}
className="hover:text-accent-dark block"
>
<h3 className="font-medium">{post.title}</h3>
<p className="text-sm text-primary-500">
{new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</p>
</Link>
</li>
))}
</ul>
</div>
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">Popular Tags</h2>
<TagList limit={15} tags={tags} />
</div>
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">Subscribe</h2>
<p className="text-primary-700 mb-4">
Stay updated with the latest posts and resources.
</p>
<form className="space-y-3">
<div>
<input
type="email"
placeholder="Your email"
className="w-full px-3 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
/>
</div>
<button
type="submit"
className="button w-full"
>
Subscribe
</button>
</form>
</div>
</aside>
);
};
export default Sidebar;
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import Link from 'next/link';
interface TagListProps {
className?: string;
limit?: number;
tags?: Array<{
tag: string;
count: number;
}>;
}
const TagList: React.FC<TagListProps> = ({ className = '', limit, tags = [] }) => {
// Sort tags by count (descending)
const sortedTags = [...tags].sort((a, b) => b.count - a.count);
// Apply limit if provided
const tagsToShow = limit ? sortedTags.slice(0, limit) : sortedTags;
return (
<div className={`flex flex-wrap gap-2 ${className}`}>
{tagsToShow.map(({ tag, count }) => (
<Link
key={tag}
href={`/tags/${tag}`}
className="bg-primary-100 text-primary-700 px-3 py-1 rounded-md hover:bg-primary-200 text-sm transition-colors duration-200 flex items-center"
>
{tag}
<span className="ml-1 text-xs bg-primary-200 rounded-full px-2 py-0.5">
{count}
</span>
</Link>
))}
</div>
);
};
export default TagList;
+114
View File
@@ -0,0 +1,114 @@
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import { PostData, PostMetadata } from '@/types';
// Make sure this is only used on the server side
const postsDirectory = path.join(process.cwd(), 'src/posts');
export function getSortedPostsData(): PostMetadata[] {
// Ensure we only run this on the server
if (typeof window === 'undefined') {
// Get file names under /posts
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames.map((fileName) => {
// Remove ".md" from file name to get id
const id = fileName.replace(/\.md$/, '');
// Read markdown file as string
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Combine the data with the id
return {
id,
title: matterResult.data.title || '',
date: matterResult.data.date || '',
excerpt: matterResult.data.excerpt || '',
author: matterResult.data.author || '',
tags: matterResult.data.tags || [],
coverImage: matterResult.data.coverImage || undefined,
} as PostMetadata;
});
// Sort posts by date
return allPostsData.sort((a, b) => {
if (a.date < b.date) {
return 1;
} else {
return -1;
}
});
}
// Return empty array if running on client side
return [];
}
export function getAllPostIds() {
// Ensure we only run this on the server
if (typeof window === 'undefined') {
const fileNames = fs.readdirSync(postsDirectory);
return fileNames.map((fileName) => {
return {
params: {
id: fileName.replace(/\.md$/, ''),
},
};
});
}
// Return empty array if running on client side
return [];
}
export async function getPostData(id: string): Promise<PostData> {
// Ensure we only run this on the server
if (typeof window === 'undefined') {
const fullPath = path.join(postsDirectory, `${id}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Use remark to convert markdown into HTML string
const processedContent = await remark()
.use(html, { sanitize: false })
.process(matterResult.content);
const contentHtml = processedContent.toString();
// Combine the data with the id and contentHtml
return {
id,
content: contentHtml,
title: matterResult.data.title || '',
date: matterResult.data.date || '',
excerpt: matterResult.data.excerpt || '',
author: matterResult.data.author || '',
tags: matterResult.data.tags || [],
coverImage: matterResult.data.coverImage || undefined,
};
}
// Return empty object if running on client side (should never happen in practice)
return {
id: '',
content: '',
title: '',
date: '',
excerpt: '',
author: '',
tags: [],
};
}
export function getPostsByTag(tag: string): PostMetadata[] {
const allPosts = getSortedPostsData();
return allPosts.filter(post => post.tags.includes(tag));
}
+6
View File
@@ -0,0 +1,6 @@
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
+74
View File
@@ -0,0 +1,74 @@
import React from 'react';
import Layout from '@/components/Layout';
const About: React.FC = () => {
return (
<Layout title="About | Confessions of Grace">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold mb-6">About</h1>
<div className="bg-white rounded-lg shadow-sm p-8 border border-primary-200 mb-10">
<h2 className="text-2xl font-bold mb-4">Confessions of Grace</h2>
<p className="text-primary-700 mb-6">
Welcome to Confessions of Grace, a blog dedicated to exploring the riches of Reformed theology
and the doctrines of grace. Our aim is to articulate timeless biblical truths in a clear,
accessible manner, helping believers understand the depth and beauty of God's sovereign grace.
</p>
<h3 className="text-xl font-bold mb-3">Our Vision</h3>
<p className="text-primary-700 mb-6">
In an age of theological confusion and spiritual relativism, we seek to provide content
that is firmly rooted in Scripture, historically informed, and pastorally sensitive.
We believe that sound doctrine leads to doxology, that theology properly understood
results in worship and wonder at the character and works of God.
</p>
<h3 className="text-xl font-bold mb-3">What We Believe</h3>
<p className="text-primary-700 mb-6">
We stand in the tradition of the Protestant Reformation, affirming the five "solas":
</p>
<ul className="list-disc list-inside mb-6 text-primary-700 space-y-2">
<li><span className="font-semibold italic">Sola Scriptura</span> Scripture Alone</li>
<li><span className="font-semibold italic">Sola Fide</span> Faith Alone</li>
<li><span className="font-semibold italic">Sola Gratia</span> Grace Alone</li>
<li><span className="font-semibold italic">Solus Christus</span> Christ Alone</li>
<li><span className="font-semibold italic">Soli Deo Gloria</span> Glory to God Alone</li>
</ul>
<p className="text-primary-700 mb-6">
We affirm the doctrines of grace as articulated in the historic Reformed confessions,
including the Westminster Standards, the Three Forms of Unity, and the 1689 London
Baptist Confession of Faith.
</p>
<blockquote className="border-l-4 border-accent pl-4 italic my-8 text-primary-600">
"For from him and through him and to him are all things. To him be glory forever. Amen."
<br /> Romans 11:36
</blockquote>
</div>
<div className="bg-white rounded-lg shadow-sm p-8 border border-primary-200">
<h2 className="text-2xl font-bold mb-4">About the Author</h2>
<div className="md:flex items-start gap-6">
<div className="md:w-1/3 mb-4 md:mb-0">
<div className="bg-primary-200 h-64 w-full rounded-md mb-4 flex items-center justify-center text-primary-500">
[Author Photo]
</div>
</div>
<div className="md:w-2/3">
<p className="text-primary-700 mb-4">
[Insert author bio here. Share your background, education, ministry experience,
and what led you to start this blog. Discuss your passion for Reformed theology
and your goals for this website.]
</p>
<p className="text-primary-700">
Feel free to reach out through the contact form or connect on social media.
</p>
</div>
</div>
</div>
</div>
</Layout>
);
};
export default About;
+87
View File
@@ -0,0 +1,87 @@
import React from 'react';
import { GetStaticProps } from 'next';
import Link from 'next/link';
import { format } from 'date-fns';
import Layout from '@/components/Layout';
import { getSortedPostsData } from '@/lib/markdown';
import { PostMetadata } from '@/types';
interface ArchiveProps {
posts: PostMetadata[];
years: number[];
}
const Archive: React.FC<ArchiveProps> = ({ posts, years }) => {
return (
<Layout title="Archive | Confessions of Grace">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold mb-8">Archive</h1>
{years.map(year => (
<div key={year} className="mb-12">
<h2 className="text-2xl font-bold border-b border-primary-200 pb-2 mb-6">{year}</h2>
<ul className="space-y-6">
{posts
.filter(post => new Date(post.date).getFullYear() === year)
.map(post => (
<li key={post.id} className="bg-white p-6 rounded-md shadow-sm border border-primary-200">
<div className="md:flex md:justify-between md:items-center">
<div>
<h3 className="text-xl font-bold mb-2">
<Link href={`/posts/${post.id}`} className="hover:text-accent-dark">
{post.title}
</Link>
</h3>
<div className="flex items-center mb-2 text-sm text-primary-500">
<span>{post.author}</span>
<span className="mx-2"></span>
<time dateTime={post.date}>
{format(new Date(post.date), 'MMMM d, yyyy')}
</time>
</div>
<p className="text-primary-600">{post.excerpt}</p>
</div>
<div className="mt-4 md:mt-0">
<Link href={`/posts/${post.id}`} className="button text-sm">
Read post
</Link>
</div>
</div>
<div className="flex flex-wrap gap-2 mt-4">
{post.tags.map(tag => (
<Link
key={tag}
href={`/tags/${tag}`}
className="text-xs bg-primary-100 text-primary-600 px-2 py-1 rounded-md hover:bg-primary-200"
>
{tag}
</Link>
))}
</div>
</li>
))}
</ul>
</div>
))}
</div>
</Layout>
);
};
export const getStaticProps: GetStaticProps = async () => {
const posts = getSortedPostsData();
// Extract unique years from post dates
const years = Array.from(new Set(
posts.map(post => new Date(post.date).getFullYear())
)).sort((a, b) => b - a); // Sort years in descending order
return {
props: {
posts,
years,
},
};
};
export default Archive;
+206
View File
@@ -0,0 +1,206 @@
import React, { useState } from 'react';
import Layout from '@/components/Layout';
const Contact: React.FC = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Basic validation
if (!name.trim() || !email.trim() || !subject.trim() || !message.trim()) {
setError('All fields are required');
return;
}
setIsSubmitting(true);
setError(null);
try {
// In a real implementation, you would send this data to your API
// For now, we'll just simulate a successful submission
await new Promise(resolve => setTimeout(resolve, 1000));
// Clear form and show success message
setName('');
setEmail('');
setSubject('');
setMessage('');
setIsSubmitted(true);
setIsSubmitting(false);
} catch (err) {
setError('Something went wrong. Please try again later.');
setIsSubmitting(false);
}
};
return (
<Layout title="Contact | Confessions of Grace">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold mb-6">Contact</h1>
<div className="bg-white rounded-lg shadow-sm p-8 border border-primary-200 mb-10">
<p className="text-primary-700 mb-6">
Have questions, comments, or suggestions? We'd love to hear from you!
Use the form below to reach out, and we'll get back to you as soon as possible.
</p>
{isSubmitted ? (
<div className="bg-green-50 border border-green-200 text-green-700 p-6 rounded-md">
<h3 className="text-xl font-bold mb-2">Thank You!</h3>
<p>Your message has been sent successfully. We'll respond to you as soon as possible.</p>
</div>
) : (
<>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md mb-6">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="name" className="block text-primary-700 mb-1">
Name <span className="text-red-500">*</span>
</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-primary-700 mb-1">
Email <span className="text-red-500">*</span>
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
/>
</div>
</div>
<div>
<label htmlFor="subject" className="block text-primary-700 mb-1">
Subject <span className="text-red-500">*</span>
</label>
<input
type="text"
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="w-full px-4 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-primary-700 mb-1">
Message <span className="text-red-500">*</span>
</label>
<textarea
id="message"
rows={6}
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full px-4 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
></textarea>
</div>
<button
type="submit"
className="button"
disabled={isSubmitting}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</>
)}
</div>
<div className="bg-white rounded-lg shadow-sm p-8 border border-primary-200">
<h2 className="text-2xl font-bold mb-4">Other Ways to Connect</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-xl font-bold mb-2">Follow</h3>
<p className="text-primary-700 mb-4">
Stay updated with our latest content on social media.
</p>
<div className="flex space-x-4">
<a
href="#"
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-accent-dark transition-colors duration-200"
aria-label="Twitter"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"></path>
</svg>
</a>
<a
href="#"
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-accent-dark transition-colors duration-200"
aria-label="Facebook"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path fillRule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clipRule="evenodd"></path>
</svg>
</a>
<a
href="#"
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-accent-dark transition-colors duration-200"
aria-label="Instagram"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path fillRule="evenodd" d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z" clipRule="evenodd"></path>
</svg>
</a>
</div>
</div>
<div>
<h3 className="text-xl font-bold mb-2">Subscribe</h3>
<p className="text-primary-700 mb-4">
Get new posts delivered to your inbox.
</p>
<form className="flex">
<input
type="email"
placeholder="Your email"
className="flex-grow px-4 py-2 rounded-l-md border border-primary-300 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
<button className="bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-r-md">
Subscribe
</button>
</form>
</div>
</div>
</div>
</div>
</Layout>
);
};
export default Contact;
+126
View File
@@ -0,0 +1,126 @@
import React from 'react';
import { GetStaticProps } from 'next';
import Layout from '@/components/Layout';
import PostCard from '@/components/PostCard';
import Sidebar from '@/components/Sidebar';
import { getSortedPostsData } from '@/lib/markdown';
import { PostMetadata } from '@/types';
interface HomeProps {
posts: PostMetadata[];
recentPosts: Array<{
id: string;
title: string;
date: string;
}>;
tags: Array<{
tag: string;
count: number;
}>;
}
const Home: React.FC<HomeProps> = ({ posts, recentPosts, tags }) => {
return (
<Layout>
<div className="mb-12 text-center">
<h1 className="text-4xl md:text-5xl font-bold mb-4">Confessions of Grace</h1>
<p className="text-xl text-primary-600 max-w-2xl mx-auto">
Exploring the doctrines of grace and the richness of Reformed theology.
</p>
</div>
<div className="flex flex-col md:flex-row gap-8">
<main className="md:w-2/3">
{posts.length > 0 && (
<div className="mb-12">
<div className="bg-white rounded-lg shadow-md overflow-hidden">
<div className="md:flex">
<div className="md:flex-shrink-0 bg-accent relative w-full md:w-64 h-64">
{posts[0].coverImage ? (
<img
src={posts[0].coverImage}
alt={posts[0].title}
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full flex items-center justify-center bg-accent text-white text-6xl font-bold">
CG
</div>
)}
</div>
<div className="p-8">
<div className="uppercase tracking-wide text-sm text-accent font-semibold">Latest Post</div>
<a href={`/posts/${posts[0].id}`} className="block mt-1 text-2xl leading-tight font-bold text-primary-900 hover:text-accent-dark">
{posts[0].title}
</a>
<p className="mt-2 text-primary-600">
{posts[0].excerpt}
</p>
<div className="mt-4">
<a href={`/posts/${posts[0].id}`} className="inline-flex items-center text-accent-dark hover:text-accent">
Read more
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
)}
<div className="mb-8">
<h2 className="text-2xl font-bold border-b border-primary-200 pb-2 mb-6">Recent Posts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{posts.slice(1).map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
<div className="text-center mt-12">
<a href="/archive" className="button">
View All Posts
</a>
</div>
</main>
<div className="md:w-1/3">
<Sidebar recentPosts={recentPosts} tags={tags} />
</div>
</div>
</Layout>
);
};
export const getStaticProps: GetStaticProps = async () => {
const posts = getSortedPostsData();
// Create tag data
const allTags = posts.flatMap(post => post.tags);
const tagCounts: Record<string, number> = {};
allTags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
const tags = Object.entries(tagCounts).map(([tag, count]) => ({
tag,
count
}));
return {
props: {
posts,
recentPosts: posts.slice(0, 5).map(post => ({
id: post.id,
title: post.title,
date: post.date
})),
tags
},
};
};
export default Home;
+149
View File
@@ -0,0 +1,149 @@
import React from 'react';
import { GetStaticProps, GetStaticPaths } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { format } from 'date-fns';
import Layout from '@/components/Layout';
import Meta from '@/components/Meta';
import ShareButtons from '@/components/ShareButtons';
import CommentSection from '@/components/CommentSection';
import { getAllPostIds, getPostData, getSortedPostsData } from '@/lib/markdown';
import { PostData, PostMetadata } from '@/types';
interface PostProps {
post: PostData;
morePosts: PostMetadata[];
}
const Post: React.FC<PostProps> = ({ post, morePosts }) => {
const postUrl = `https://confessionsofgrace.com/posts/${post.id}`;
return (
<Layout
title={`${post.title} | Confessions of Grace`}
description={post.excerpt}
>
<Meta
title={post.title}
description={post.excerpt}
keywords={post.tags.join(', ')}
image={post.coverImage}
url={postUrl}
type="article"
/>
<article className="max-w-3xl mx-auto">
<header className="mb-8">
<h1 className="text-3xl md:text-4xl font-bold mb-2">{post.title}</h1>
<div className="flex items-center text-primary-500 mb-6">
<span>{post.author}</span>
<span className="mx-2"></span>
<time dateTime={post.date}>
{format(new Date(post.date), 'MMMM d, yyyy')}
</time>
</div>
{post.coverImage && (
<div className="relative h-64 md:h-96 w-full mb-8 rounded-lg overflow-hidden">
<Image
src={post.coverImage}
alt={post.title}
fill
className="object-cover"
priority
/>
</div>
)}
<div className="flex flex-wrap gap-2 mb-6">
{post.tags.map(tag => (
<Link
key={tag}
href={`/tags/${tag}`}
className="text-sm bg-primary-100 text-primary-600 px-3 py-1 rounded-md hover:bg-primary-200"
>
{tag}
</Link>
))}
</div>
</header>
<div
className="blog-post prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<div className="mt-8 pt-6 border-t border-primary-200">
<ShareButtons
url={postUrl}
title={post.title}
description={post.excerpt}
/>
</div>
<div className="mt-12 pt-6 border-t border-primary-200">
<h2 className="text-2xl font-bold mb-6">More Posts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{morePosts.slice(0, 2).map(post => (
<div key={post.id} className="bg-white p-6 rounded-md border border-primary-200 shadow-sm">
<h3 className="font-bold mb-2">
<Link href={`/posts/${post.id}`} className="hover:text-accent-dark">
{post.title}
</Link>
</h3>
<p className="text-primary-600 text-sm mb-2">
{format(new Date(post.date), 'MMMM d, yyyy')}
</p>
<p className="text-primary-700">{post.excerpt}</p>
</div>
))}
</div>
</div>
<CommentSection postId={post.id} />
<div className="mt-12 pt-6 border-t border-primary-200">
<Link href="/" className="text-accent-dark hover:text-accent inline-flex items-center">
<svg className="mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to all posts
</Link>
</div>
</article>
</Layout>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
const paths = getAllPostIds();
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = await getPostData(params?.id as string);
const allPosts = getSortedPostsData();
// Filter out the current post and get a few related posts (by tags)
const otherPosts = allPosts.filter(p => p.id !== post.id);
// Find posts with matching tags
const relatedPosts = otherPosts
.filter(p => p.tags.some(tag => post.tags.includes(tag)))
.slice(0, 2);
// If we don't have enough related posts, add recent posts
const morePosts = relatedPosts.length < 2
? [...relatedPosts, ...otherPosts.filter(p => !relatedPosts.includes(p))].slice(0, 2)
: relatedPosts;
return {
props: {
post,
morePosts,
},
};
};
export default Post;
+128
View File
@@ -0,0 +1,128 @@
import React from 'react';
import Layout from '@/components/Layout';
interface Resource {
title: string;
author: string;
description: string;
link?: string;
category: string;
}
const Resources: React.FC = () => {
const resources: Resource[] = [
{
title: "Institutes of the Christian Religion",
author: "John Calvin",
description: "Calvin's magnum opus on systematic theology, covering the doctrines of God, man, salvation, and the church.",
category: "Books"
},
{
title: "The Bondage of the Will",
author: "Martin Luther",
description: "Luther's defense of the doctrine of total depravity and God's sovereignty in salvation.",
category: "Books"
},
{
title: "Chosen by God",
author: "R.C. Sproul",
description: "A accessible introduction to the doctrine of predestination.",
category: "Books"
},
{
title: "The Westminster Confession of Faith",
author: "Westminster Assembly",
description: "A historic Reformed confession of faith that remains influential today.",
link: "https://www.pcaac.org/bco/westminster-confession/",
category: "Confessions"
},
{
title: "The Heidelberg Catechism",
author: "Zacharias Ursinus & Caspar Olevianus",
description: "A warm, pastoral Reformed catechism organized around comfort in Christ.",
link: "https://www.ligonier.org/learn/articles/heidelberg-catechism",
category: "Confessions"
},
{
title: "Ligonier Ministries",
author: "Founded by R.C. Sproul",
description: "A ministry dedicated to helping Christians know what they believe, why they believe it, how to live it, and how to share it.",
link: "https://www.ligonier.org/",
category: "Websites"
},
{
title: "Monergism",
author: "",
description: "A comprehensive resource for Reformed theology, including articles, books, and audio resources.",
link: "https://www.monergism.com/",
category: "Websites"
}
];
const categories = Array.from(new Set(resources.map(resource => resource.category)));
return (
<Layout title="Resources | Confessions of Grace">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold mb-6">Reformed Resources</h1>
<p className="text-lg text-primary-700 mb-10">
This is a curated collection of resources related to Reformed theology and the doctrines of grace.
These books, confessions, and websites have been formative in my own theological journey and
are recommended for those seeking to deepen their understanding of Reformed thought.
</p>
{categories.map(category => (
<div key={category} className="mb-12">
<h2 className="text-2xl font-bold border-b border-primary-200 pb-2 mb-6">{category}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{resources
.filter(resource => resource.category === category)
.map((resource, index) => (
<div key={index} className="bg-white rounded-lg shadow-sm p-6 border border-primary-200">
<h3 className="text-xl font-bold mb-2">{resource.title}</h3>
{resource.author && (
<p className="text-primary-500 italic mb-3">by {resource.author}</p>
)}
<p className="text-primary-700 mb-4">{resource.description}</p>
{resource.link && (
<a
href={resource.link}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-accent-dark hover:text-accent"
>
Visit Resource
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 ml-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
)}
</div>
))}
</div>
</div>
))}
<div className="bg-primary-100 rounded-lg p-6 border border-primary-200 mt-10">
<h2 className="text-xl font-bold mb-4">Suggest a Resource</h2>
<p className="text-primary-700 mb-4">
Do you have a resource suggestion that would be valuable for readers of this blog?
Please use the contact form to share your recommendations.
</p>
<a href="#" className="button inline-block">
Contact
</a>
</div>
</div>
</Layout>
);
};
export default Resources;
+94
View File
@@ -0,0 +1,94 @@
import React, { useState, useEffect } from 'react';
import { GetStaticProps } from 'next';
import { useRouter } from 'next/router';
import Layout from '@/components/Layout';
import PostCard from '@/components/PostCard';
import SearchBar from '@/components/SearchBar';
import { getSortedPostsData } from '@/lib/markdown';
import { PostMetadata } from '@/types';
interface SearchPageProps {
allPosts: PostMetadata[];
}
const SearchPage: React.FC<SearchPageProps> = ({ allPosts }) => {
const router = useRouter();
const { q: query } = router.query;
const [searchResults, setSearchResults] = useState<PostMetadata[]>([]);
useEffect(() => {
if (typeof query !== 'string' || !query.trim()) {
setSearchResults([]);
return;
}
const searchTerms = query.toLowerCase().trim().split(/\s+/);
const filteredPosts = allPosts.filter(post => {
const titleMatch = searchTerms.some(term =>
post.title.toLowerCase().includes(term)
);
const excerptMatch = searchTerms.some(term =>
post.excerpt.toLowerCase().includes(term)
);
const tagMatch = post.tags.some(tag =>
searchTerms.some(term => tag.toLowerCase().includes(term))
);
return titleMatch || excerptMatch || tagMatch;
});
setSearchResults(filteredPosts);
}, [query, allPosts]);
return (
<Layout title={`Search: ${query || ''} | Confessions of Grace`}>
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold mb-6">Search Results</h1>
<div className="mb-8">
<SearchBar />
</div>
{typeof query === 'string' && query.trim() ? (
<>
<p className="mb-8 text-primary-600">
{searchResults.length} {searchResults.length === 1 ? 'result' : 'results'} for "{query}"
</p>
{searchResults.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{searchResults.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
) : (
<div className="bg-white rounded-lg shadow-sm p-8 border border-primary-200 text-center">
<p className="text-xl text-primary-600 mb-4">No results found for "{query}"</p>
<p className="text-primary-500">Try using different keywords or browse all posts.</p>
</div>
)}
</>
) : (
<div className="bg-white rounded-lg shadow-sm p-8 border border-primary-200">
<p className="text-xl text-primary-600 mb-4 text-center">Enter a search term to find posts</p>
</div>
)}
</div>
</Layout>
);
};
export const getStaticProps: GetStaticProps = async () => {
const allPosts = getSortedPostsData();
return {
props: {
allPosts,
},
};
};
export default SearchPage;
+66
View File
@@ -0,0 +1,66 @@
import React from 'react';
import { GetStaticProps, GetStaticPaths } from 'next';
import Layout from '@/components/Layout';
import PostCard from '@/components/PostCard';
import { getSortedPostsData, getPostsByTag } from '@/lib/markdown';
import { PostMetadata } from '@/types';
interface TagPageProps {
posts: PostMetadata[];
tag: string;
}
const TagPage: React.FC<TagPageProps> = ({ posts, tag }) => {
return (
<Layout title={`${tag} | Confessions of Grace`}>
<div className="max-w-4xl mx-auto">
<div className="mb-10">
<h1 className="text-3xl md:text-4xl font-bold mb-4">Tag: {tag}</h1>
<p className="text-lg text-primary-600">
{posts.length} {posts.length === 1 ? 'post' : 'posts'} tagged with "{tag}"
</p>
</div>
{posts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
) : (
<div className="text-center py-12">
<p className="text-xl text-primary-500">No posts found with this tag.</p>
</div>
)}
</div>
</Layout>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
const posts = getSortedPostsData();
const tags = Array.from(new Set(posts.flatMap(post => post.tags)));
const paths = tags.map((tag) => ({
params: { tag },
}));
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const tag = params?.tag as string;
const posts = getPostsByTag(tag);
return {
props: {
posts,
tag,
},
};
};
export default TagPage;
+72
View File
@@ -0,0 +1,72 @@
import React from 'react';
import { GetStaticProps } from 'next';
import Link from 'next/link';
import Layout from '@/components/Layout';
import { getSortedPostsData } from '@/lib/markdown';
import { PostMetadata } from '@/types';
interface TagsPageProps {
tags: {
name: string;
count: number;
}[];
}
const TagsPage: React.FC<TagsPageProps> = ({ tags }) => {
return (
<Layout title="Tags | Confessions of Grace">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold mb-8">Browse by Tag</h1>
<div className="bg-white rounded-lg shadow-sm p-8 border border-primary-200">
<div className="flex flex-wrap gap-3">
{tags.map(tag => (
<Link
key={tag.name}
href={`/tags/${tag.name}`}
className="bg-primary-100 hover:bg-primary-200 text-primary-700 px-4 py-2 rounded-md text-lg transition-colors duration-200 flex items-center"
>
{tag.name}
<span className="ml-2 bg-primary-200 rounded-full px-2 py-0.5 text-sm">
{tag.count}
</span>
</Link>
))}
</div>
</div>
<div className="mt-12 text-center">
<Link href="/archive" className="button">
View All Posts
</Link>
</div>
</div>
</Layout>
);
};
export const getStaticProps: GetStaticProps = async () => {
const posts = getSortedPostsData();
// Extract all tags from posts
const allTags = posts.flatMap(post => post.tags);
// Count occurrences of each tag
const tagCounts = allTags.reduce((acc, tag) => {
acc[tag] = (acc[tag] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// Convert to array and sort alphabetically
const tags = Object.entries(tagCounts)
.map(([name, count]) => ({ name, count }))
.sort((a, b) => a.name.localeCompare(b.name));
return {
props: {
tags,
},
};
};
export default TagsPage;
+124
View File
@@ -0,0 +1,124 @@
---
title: "Sola Fide: Justification by Faith Alone"
date: "2025-02-15"
author: "Austin Bennett"
excerpt: "Understanding the Reformation doctrine of justification by faith alone and its central importance to the gospel."
tags: ["Sola Fide", "Justification", "Reformation", "Doctrine"]
coverImage: "/images/faith-alone.svg"
---
# Sola Fide: Justification by Faith Alone
> "For we hold that one is justified by faith apart from works of the law." — Romans 3:28
## Introduction
At the heart of the Protestant Reformation stands the recovery of a profound biblical truth: justification by faith alone (*sola fide*). This doctrine addresses the most fundamental question any human can ask: "How can a sinner be right with a holy God?"
The Reformers' answer, drawn from Scripture, was revolutionary in its day and remains life-changing in ours: we are justified—declared righteous before God—through faith alone in Christ alone, apart from any works we might perform.
## The Meaning of Justification
Before exploring faith's role in justification, we must understand what justification itself means. The term comes from the legal sphere and refers to a declaration of righteousness. When God justifies sinners, He declares them righteous in His sight.
Importantly, justification is not the same as sanctification. While sanctification is the gradual process of becoming more like Christ, justification is an instantaneous legal declaration. As the Reformers often put it, justification is not God making us righteous (that's sanctification) but declaring us righteous.
## Faith as the Instrument of Justification
Scripture consistently teaches that faith is the instrument through which we receive justification:
- Romans 5:1: "Therefore, since we have been justified by faith, we have peace with God through our Lord Jesus Christ."
- Galatians 2:16: "Yet we know that a person is not justified by works of the law but through faith in Jesus Christ."
- Philippians 3:9: "[That I may] be found in him, not having a righteousness of my own that comes from the law, but that which comes through faith in Christ."
But what exactly is this justifying faith? The Reformers identified three essential components:
1. **Knowledge (notitia)**: Faith involves intellectual knowledge of the gospel message.
2. **Assent (assensus)**: Faith includes intellectual agreement that the gospel is true.
3. **Trust (fiducia)**: Most importantly, faith entails personal trust in Christ for salvation.
True saving faith, then, is not merely intellectual assent to facts about Jesus but personal reliance on Him and His finished work.
## The "Alone" in Faith Alone
The word "alone" (Latin: *sola*) in "faith alone" is crucial. It means that nothing else serves as the instrument of justification—not works, not sacraments, not the church, but faith only.
This doesn't mean that faith exists in isolation. As the Reformers emphasized, justifying faith is never alone in the person justified—it always produces good works. As Martin Luther put it, "We are saved by faith alone, but the faith that saves is never alone."
The distinction is between the ground of justification (Christ's righteousness) and the evidence of justification (our works). Works are the necessary result of justification, not its cause or even its instrument.
## Historical Context
The doctrine of justification by faith alone emerged in stark contrast to the medieval Catholic understanding, which viewed justification as a process involving both faith and works. For medieval theologians, justification included both the forgiveness of sins and the infusion of righteousness, making it virtually synonymous with sanctification.
Martin Luther's breakthrough came through his study of Romans 1:17: "For in it [the gospel] the righteousness of God is revealed from faith for faith, as it is written, 'The righteous shall live by faith.'" Luther realized that the "righteousness of God" was not something we had to achieve but something God freely gives to those who believe.
This insight changed everything. No longer was salvation a matter of accumulating merit through good works. Instead, it was received as a gift through faith in Christ, whose perfect righteousness is credited to believers.
## Biblical Case for Faith Alone
The biblical argument for justification by faith alone rests on several foundations:
### 1. The Testimony of Paul
Paul explicitly contrasts faith and works as means of justification:
"For by works of the law no human being will be justified in his sight, since through the law comes knowledge of sin. But now the righteousness of God has been manifested apart from the law... the righteousness of God through faith in Jesus Christ for all who believe." (Romans 3:20-22)
### 2. The Example of Abraham
In Romans 4, Paul points to Abraham as the paradigmatic example of justification by faith:
"For what does the Scripture say? 'Abraham believed God, and it was counted to him as righteousness.'" (Romans 4:3)
Significantly, Paul notes that Abraham was justified before his circumcision, proving that justification doesn't depend on religious rituals.
### 3. The Nature of Faith and Works
Faith and works function differently in relation to justification. Faith is receptive—it receives Christ's righteousness as a gift. Works are active—they attempt to earn God's favor. The two principles are mutually exclusive. As Paul writes in Romans 4:4-5:
"Now to the one who works, his wages are not counted as a gift but as his due. And to the one who does not work but believes in him who justifies the ungodly, his faith is counted as righteousness."
## Implications of Faith Alone
The doctrine of justification by faith alone carries profound implications:
### 1. Assurance
If justification depends on our works, we can never have assurance of salvation, since we can never be sure we've done enough. But if justification comes through faith alone in Christ alone, we can have confidence based on His finished work rather than our imperfect efforts.
### 2. Freedom
The gospel liberates us from the crushing burden of trying to earn God's favor. We're freed to serve God from gratitude rather than fear.
### 3. Humility
When we recognize that justification comes through faith alone, pride becomes impossible. We cannot boast about our salvation, since it's entirely God's gift rather than our achievement.
### 4. Unity
Justification by faith alone creates a level playing field. The sophisticated theologian and the simple believer stand justified on exactly the same grounds: faith in Christ. This undermines religious elitism and fosters true spiritual unity.
## Objections Addressed
Critics of justification by faith alone often cite James 2:24: "You see that a person is justified by works and not by faith alone." However, this apparent contradiction dissolves when we recognize that Paul and James use the term "justified" differently.
Paul speaks of justification before God, while James addresses justification before men. James combats a "faith" that is mere intellectual assent without life transformation. He insists that true faith manifests itself in works. This aligns perfectly with the Reformed understanding that while faith alone justifies, the faith that justifies is never alone.
## Conclusion
Justification by faith alone remains the heart of the gospel. It reminds us that salvation is God's work from beginning to end—He provides the sacrifice in Christ, and He gives the faith through which we receive Christ's righteousness.
In an age of achievement and self-help, this doctrine stands as a radical affirmation that we contribute nothing to our salvation except the sin that makes it necessary. And in that truth, paradoxically, we find our greatest hope.
As J.I. Packer eloquently summarized:
"The way to be right with God and to be justified is to trust in Christ alone as the One who died, bearing our sin and curse upon Himself, to bring us to God. ... All who put their faith in Christ can know that they are justified, now and forever."
---
**Questions for Reflection:**
1. How does understanding justification by faith alone affect your daily walk with God?
2. In what subtle ways might you still be tempted to base your standing with God on your performance?
3. How would you explain the difference between "faith alone" and "faith that is alone" to someone new to Reformed theology?
+40
View File
@@ -0,0 +1,40 @@
---
title: "Frank Turek & The Fourfold State of Man"
date: "2025-03-18"
author: "@Auggie2LBCF"
excerpt: "The 'contradiction' that Geisler suggests, is not a contradiction but an incomplete understanding of the abilities of Adam, the abilities of Adam's offspring, and the abilities of those who receive God's abundant provision of grace, who are given the responsibility to freely offer that abundant provision of grace to all those who are Adam's offspring."
tags: ["#sin", "#calvinism", "creation", "fourfold"]
coverImage: "/images/thomas-boston.jpg"
---
If you are looking for a definitive answer to the question "Where did Sin Come From?" please press the "close tab" button, it is usually marked with an "x" on your browser or perhaps you are on a phone and need to hit the little box first. Either way the origin of sin is not clearly presented in the Bible.
I was scrolling throu[]()gh Instagram Reels one day (also known by other names such as "a bad idea" and "a waste of time") and happened on a [video by Frank Turek](https://youtube.com/shorts/qdiW5dtOkgo?si=YuiUgg5uZwkzcuD3). In it he is doing a form of Q&A at what looks to be a university. And what it said really took me off guard and made me think (which rarely happens when scrolling through any short video platform). Here is the transcript of that video:
> **Q:** "My in-laws are Calvinist. I want to know what the main out of the Five Points, what is the main thing that's, for like of a better term, wrong what's wrong about Calvinism?"
>
> **Frank Turek:** "See, the ultimate problem with Calvinism, hard five point Calvinism in my view, is it makes the world a sham. Because we really don't have a free choice, but God is telling us that we ought to choose him when we can't choose him because he hasn't chosen us at all. And secondly, it makes God the author of evil. In fact, let me give you an example. In a debate that took place 40 years ago at Dallas Theological Seminary, it was between Norman Geisler, my mentor, and a guy by name of John Gerstner. At one point, Geisler turned to Gerstner and he said, 'Does man have free will?' And Gerstner said, 'Yes, man has free will to do what he desires, but God gives him the desire of his heart.' So Geisler said, 'Who gave Adam the desire to sin?' and Gerstner said, 'Mystery.' And Geisler said, 'Contradiction.'"
The debate that Turek mentions is not recorded anywhere but in Geisler's book *Chosen But Free*.
> Many years ago when the late John Gerstner and I taught together at the same institution, I invited him into one of my classes to discuss free will. Being what I have called an extreme Calvinist, he defended Jonathan Edwards' view that the human will is moved by the strongest desire. I will never forget how he responded when I pushed the logic all the way back to Lucifer. I was stunned to hear an otherwise very rational man respond to my question "Who gave Lucifer the desire to rebel against God?" by throwing up his hands and crying, "Mystery, mystery, a great mystery!" I answered, "No, it is not a great mystery; it is a grave contradiction." And this is because, on the premises of extreme Calvinism, only God could have given Lucifer the desire to rebel against God, since there is no self-determined free choice and Lucifer had no evil nature. But if this is so, then logically it must have been God who gave him the desire to sin. In short, God caused a rebellion against God! Perish the thought!
The debate that Turek and Geisler mention is presented as a "gotcha" moment that completely and utterly destroys all arguments for Calvinism and should shut up the Cage-Stagers. Cage-Stagers is a term which here means: a person who should be locked in a cage until they can have a civil conversation about Calvinism. However, this is missing the response. And as I have yet to be able to find the response that Gerstner gives, I will write one here. *The "contradiction" that Geisler suggests, is not a contradiction but an incomplete understanding of the abilities of Adam, the abilities of Adam's offspring, and the abilities of those who receive God's abundant provision of grace, who are given the responsibility to freely offer that abundant provision of grace to all those who are Adam's offspring.*
Abilities is a word which, in the murky and complicated world of theological discourse, refers to a peculiar and somewhat precarious collection of potential actions, much like a rickety ladder balanced precariously over the deep and treacherous chasm of human limitation—in this case, specifically applying to Adam and Eve in their initial state, mankind after their unfortunate fall from grace, believers, and those who have already departed to be with the lord. Thomas Boston refers to these as the fourfold state of man.
### 1. Man Created (*status integritiatis*) or Primitive Integrity
Man was created in a state of innocence. "This only have I found: God created mankind upright, but they have gone in search of many schemes." (Ecclesiastes 7:29). However, Adam also ate the apple and fell from this upright position, thus cursing all of man. "Therefore, just as sin entered the world through one man, and death through sin, and in this way death came to all people, because all sinned." (Romans 5:12). From these passages we can see that God created Adam with the ability to do upright actions, but with the inclusion of Romans 5:12, we can see that Adam was able to not sin, being upright, or able to sin. Augustine refers to this as *posse peccare, posse non peccare*.
### 2. Man Fallen (*status corruptionis*) or Entire Depravity
From Adam's sin, death came to all people (Romans 5:12). Each one of us as Adam's offspring are cursed with this death. "As for you, you were dead in your transgressions and sins... All of us also lived among them at one time, gratifying the cravings of our flesh... and were by nature deserving of wrath." (Ephesians 2:1-3). We are dead in our transgressions. We are from birth children of wrath who seek only to gratify the cravings of our flesh. We are unable not to sin (*non posse non peccare*). Regardless of how our actions look to other men, the Bible is clear: "None is righteous, no, not one; no one understands; no one seeks for God. All have turned aside; together they have become worthless; no one does good, not even one." (Rom. 3:10-12). No one does good.
### 3. Man Redeemed (*status gratiae*) or Begun Recovery
However, in God's curse to Adam and Eve, he also gave them hope for their salvation in the one who would crush the serpent's head. Noah's father Lamech was hoping for the "one who would bring relief" (Genesis 5:29). The one who would bring relief was Christ, who, through his work on Calvary, took the cup of wrath that all deserve and bought his people (1 Corinthians 7:23) into a new creation where we have the ability to not sin (*posse non peccare*). "Therefore, if anyone is in Christ, the new creation has come: The old has gone, the new is here!" (2 Corinthians 5:17).
### 4. Man Glorified (*status gloriae*) or Consummate Happiness or Misery
However, we still sin. "Not only so, but we ourselves, who have the first fruits of the Spirit, groan inwardly as we wait eagerly for our adoption to sonship, the redemption of our bodies." (Romans 8:23). We will not be fully transformed until we are in Heaven: "we will transform our lowly bodies so that they will be like his glorious body." (Philippians 3:21).
So, Turek and Geisler misunderstand that Adam was not cursed in the way that we are. All of our desires are for the cravings of our flesh. Calvinism does not teach that God is the author of evil. The Bible teaches that our hearts are worthless and need the Holy Spirit to save us: "he saved us, not because of works done by us in righteousness, but according to his own mercy, by the washing of regeneration and renewal of the Holy Spirit" (Titus 3:5).
As to Turek's claim that "God is telling us that we ought to choose him when we can't choose him because he hasn't chosen us," he is missing that anyone who wants to choose God will be able to choose him. The Holy Spirit must renew us. It is not a work of our righteousness to choose God but the regeneration of the Holy Spirit through the proclamation of the Gospel.
+92
View File
@@ -0,0 +1,92 @@
---
title: "Sola Gratia: Salvation by Grace Alone"
date: "2025-03-15"
author: "Author Name"
excerpt: "Exploring the Reformation principle of salvation by grace alone and its implications for our understanding of God and ourselves."
tags: ["Sola Gratia", "Reformation", "Soteriology", "Doctrine"]
coverImage: "/images/grace-alone.svg"
---
# Sola Gratia: Salvation by Grace Alone
> "For by grace you have been saved through faith. And this is not your own doing; it is the gift of God, not a result of works, so that no one may boast." — Ephesians 2:8-9
## Introduction
Among the five _solas_ of the Reformation, perhaps none more directly confronts human pride than the doctrine of _sola gratia_ — by grace alone. This teaching stands as a powerful reminder that our salvation is entirely a gift, unmerited and unearned, flowing solely from God's boundless mercy and goodness.
In a culture that celebrates achievement, self-sufficiency, and personal merit, the doctrine of grace alone remains as counter-cultural and necessary today as it was in the 16th century. This post explores the biblical foundations of this doctrine and its implications for our spiritual lives.
## Biblical Foundations
The Apostle Paul, writing to the Ephesians, could not be clearer: salvation is "by grace... through faith," and even this faith is "not your own doing" but rather "the gift of God" (Eph. 2:8). The exclusivity of grace is emphasized by the phrase "not a result of works," eliminating any ground for human boasting.
Throughout Scripture, we find this theme repeated:
- Romans 3:24 affirms that we are "justified by his grace as a gift, through the redemption that is in Christ Jesus."
- Titus 3:5-7 reminds us that God "saved us, not because of works done by us in righteousness, but according to his own mercy."
- Acts 15:11 declares that "we believe that we will be saved through the grace of the Lord Jesus."
The cumulative witness of Scripture is that salvation is fundamentally a divine work, not a human achievement. It originates in God's loving choice, not our spiritual performance.
## Historical Context
During the medieval period, the Church had developed a complex system of penance, indulgences, and merit that obscured the free nature of God's grace. Salvation was often presented as a cooperative effort between God and humans, with divine grace making salvation possible but human works making it actual.
Luther, Calvin, and other Reformers recovered the Augustinian emphasis on the necessity and sufficiency of grace. They insisted that salvation is God's work from beginning to end. Luther famously wrote:
> "This grace of God is a very great, strong, mighty and active thing. It does not lie asleep in the soul. Grace hears, leads, drives, draws, changes, works all in man, and lets itself be distinctly felt and experienced. It is hidden, but its works are evident."
## Theological Implications
The doctrine of grace alone carries several important implications:
### 1. Human Inability
_Sola gratia_ presupposes human inability to contribute to salvation. The Reformed tradition speaks of "total depravity" — not that humans are as evil as they could be, but rather that sin has affected every aspect of our being, including our will. As Augustine famously prayed: "Command what you will, and grant what you command."
### 2. Divine Sovereignty
If salvation is by grace alone, then God must be sovereign in dispensing this grace. The doctrine points to God's freedom in election and predestination, concepts that, while challenging, underscore the gratuitous nature of salvation.
### 3. Assurance
When salvation depends entirely on God's grace rather than human performance, believers can have genuine assurance. Our standing before God rests not on our fluctuating obedience but on His unchanging promise.
## Practical Applications
Far from being merely theoretical, _sola gratia_ transforms how we live the Christian life:
### 1. Humility
Understanding grace alone breeds humility. We recognize that we bring nothing to our salvation — it is entirely God's gift. This undermines spiritual pride and self-righteousness.
### 2. Gratitude
When we truly grasp that salvation is by grace alone, gratitude becomes the dominant note in our spiritual lives. We respond to God's gracious gift with thankfulness rather than attempting to earn His favor.
### 3. Freedom
The doctrine liberates us from performance-based religion. We serve God not to gain His acceptance but because we already have it in Christ. This transforms obedience from burdensome duty to joyful response.
### 4. Evangelism
Grace alone affects how we share the gospel. We offer others not a program of self-improvement but the good news of what God has done in Christ.
## Conclusion
In a world of achievement and merit, _sola gratia_ remains a radical, counter-cultural doctrine. It reminds us that the Christian faith is not about what we do for God but about what He has done for us in Christ.
As J.I. Packer wrote:
> "Grace is God drawing sinners closer and closer to himself. How does God in grace prosecute this purpose? Not by shielding us from assault by the work, the flesh, and the devil, nor by protecting us from burdensome and frustrating circumstances, not yet by shielding us from troubles created by our own temperament and psychology, but rather by exposing us to all these things, so as to overwhelm us with a sense of our own inadequacy, and to drive us to cling to him more closely."
May we, like the Reformers before us, recover and rejoice in the biblical truth that salvation is by grace alone.
---
**Questions for Reflection:**
1. How has understanding God's grace changed your approach to the Christian life?
2. In what ways might you still be tempted to rely on your own spiritual performance rather than God's grace?
3. How does the doctrine of grace alone affect your view of others, particularly those who struggle spiritually?
+112
View File
@@ -0,0 +1,112 @@
---
title: "The Sovereignty of God in an Age of Uncertainty"
date: "2025-03-01"
author: "Author Name"
excerpt: "Finding comfort and stability in God's sovereign rule over all things during times of personal and global uncertainty."
tags: ["Sovereignty", "Providence", "Theology", "Comfort"]
coverImage: "/images/sovereignty.svg"
---
# The Sovereignty of God in an Age of Uncertainty
> "Our God is in the heavens; he does all that he pleases." — Psalm 115:3
In an age characterized by uncertainty—whether economic instability, political polarization, global conflicts, or personal trials—the doctrine of God's sovereignty stands as an anchor for the soul. This foundational biblical truth reminds us that above all the chaos and confusion of our world reigns a God who is absolutely sovereign, whose purposes cannot be thwarted, and whose plans will ultimately prevail.
## What is Divine Sovereignty?
Divine sovereignty refers to God's absolute rule over all creation. It means that God is the ultimate authority and is in complete control of everything that happens. Nothing occurs outside His knowledge or permission. As the Westminster Confession eloquently states:
> "God from all eternity, did, by the most wise and holy counsel of His own will, freely, and unchangeably ordain whatsoever comes to pass; yet so, as thereby neither is God the author of sin, nor is violence offered to the will of the creatures; nor is the liberty or contingency of second causes taken away, but rather established."
This doctrine encompasses several key truths:
1. **God's sovereignty is universal** - It extends over all creation, all nations, all history, and all individuals.
2. **God's sovereignty is absolute** - No creature or circumstance can ultimately thwart His will.
3. **God's sovereignty is wise** - His rule is not arbitrary but guided by perfect wisdom.
4. **God's sovereignty is good** - His control is exercised for His glory and the good of His people.
## Biblical Foundations
Scripture repeatedly affirms God's sovereign rule:
- **Over creation**: "In the beginning, God created the heavens and the earth" (Genesis 1:1)
- **Over nations**: "The Most High rules the kingdom of men and gives it to whom he will" (Daniel 4:17)
- **Over individual lives**: "Your eyes saw my unformed substance; in your book were written, every one of them, the days that were formed for me, when as yet there was none of them" (Psalm 139:16)
- **Over salvation**: "Even as he chose us in him before the foundation of the world" (Ephesians 1:4)
- **Over suffering**: "I form light and create darkness; I make well-being and create calamity; I am the LORD, who does all these things" (Isaiah 45:7)
The Bible portrays a God who is actively involved in all events, working everything "according to the counsel of his will" (Ephesians 1:11).
## Sovereignty and Human Responsibility
One of the most profound mysteries in Scripture is the relationship between God's sovereignty and human responsibility. The Bible clearly teaches both: God is completely sovereign, yet humans make real choices for which they are responsible.
Rather than viewing these as contradictory truths, Reformed theology sees them as complementary. God's sovereignty does not eliminate human choice but rather establishes the context in which meaningful choice can occur. As Augustine put it, "God is not the author of the evil you do, though he is the author of your being while you do it."
Consider the story of Joseph. His brothers acted wickedly in selling him into slavery, yet Joseph later declared, "You meant evil against me, but God meant it for good" (Genesis 50:20). Both statements are true simultaneously—human evil and divine purpose coexisted in the same events.
## The Comfort of Divine Sovereignty
The doctrine of God's sovereignty provides tremendous comfort in times of uncertainty:
### 1. Assurance of Purpose
In a seemingly random universe, we can be confident that everything happens for a reason. As Romans 8:28 promises, "We know that for those who love God all things work together for good, for those who are called according to his purpose."
### 2. Freedom from Anxiety
Jesus teaches, "Do not be anxious about your life" (Matthew 6:25). This command is grounded in the reality of God's sovereign care. If God controls all things, then anxiety becomes unnecessary. We can trust our Father's wise governance.
### 3. Courage in Suffering
Suffering becomes more bearable when we recognize it's not random or meaningless. The sovereignty of God means our pain is purposeful. As C.S. Lewis observed, "God whispers to us in our pleasures, speaks in our consciences, but shouts in our pains. It is his megaphone to rouse a deaf world."
### 4. Hope in Evangelism
When sharing the gospel, we're not dependent on our persuasive abilities alone. God is sovereignly drawing people to Himself. As Jesus said, "No one can come to me unless the Father who sent me draws him" (John 6:44).
## Living Under God's Sovereignty
How should we live in light of God's sovereignty?
### 1. Humble Submission
Recognizing God's sovereignty should lead to humble acceptance of His will, even when we don't understand it. Like Christ in Gethsemane, we pray, "Not my will, but yours, be done" (Luke 22:42).
### 2. Prayerful Dependence
Far from making prayer unnecessary, God's sovereignty establishes its foundation. We pray precisely because we know God has the power to act. As the Westminster Shorter Catechism states, "Prayer is an offering up of our desires unto God, for things agreeable to his will."
### 3. Grateful Worship
Understanding God's sovereignty should evoke profound gratitude and worship. If all good things come from His hand, our response should be thankfulness rather than entitlement.
### 4. Confident Obedience
We can obey God's commands with confidence, knowing that He sovereignly enables what He requires. As Philippians 2:12-13 puts it, "Work out your own salvation with fear and trembling, for it is God who works in you, both to will and to work for his good pleasure."
## Sovereignty in Times of Crisis
In our modern world fraught with uncertainty, the doctrine of divine sovereignty provides a stable foundation. When financial systems collapse, when health fails, when relationships fracture—nothing catches God by surprise. He remains on His throne.
The COVID-19 pandemic, political upheavals, economic instabilities, and personal tragedies—all fall under God's sovereign governance. This doesn't make these troubles disappear, but it does transform how we experience them. We face them not as victims of chance but as children of a sovereign Father who works all things for our ultimate good.
As Charles Spurgeon beautifully stated:
> "There is no attribute more comforting to His children than that of God's sovereignty. Under the most adverse circumstances, in the most severe trials, they believe that sovereignty has ordained their afflictions, that sovereignty overrules them, and that sovereignty will sanctify them all."
## Conclusion
In an age characterized by uncertainty, the doctrine of God's sovereignty provides an unshakable foundation. When everything else seems to be shifting sand, we stand on the rock of God's absolute, wise, and good control over all things.
Let us find comfort in knowing that our lives are not determined by blind fate, random chance, or human decisions alone, but by a loving Father who "works all things according to the counsel of his will" (Ephesians 1:11).
---
**Questions for Reflection:**
1. How has God's sovereignty been displayed in your own life circumstances?
2. In what specific area of uncertainty are you struggling to trust God's sovereign control?
3. How might you respond differently to current trials if you truly believed in God's good sovereignty?
4. What comfort can you draw from God's sovereignty as you face an unknown future?
+51
View File
@@ -0,0 +1,51 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-primary-50 text-primary-800 font-serif;
}
h1, h2, h3, h4, h5, h6 {
@apply font-serif text-primary-900 mb-4 font-bold;
}
h1 {
@apply text-3xl md:text-4xl;
}
h2 {
@apply text-2xl md:text-3xl;
}
h3 {
@apply text-xl md:text-2xl;
}
a {
@apply text-accent-dark hover:text-accent transition-colors duration-200;
}
blockquote {
@apply border-l-4 border-accent pl-4 italic my-4;
}
}
@layer components {
.blog-post {
@apply prose prose-lg max-w-none;
}
.blog-post h1, .blog-post h2, .blog-post h3 {
@apply border-b border-primary-200 pb-2;
}
.button {
@apply px-4 py-2 rounded-md bg-accent text-white shadow-sm hover:bg-accent-dark transition-colors duration-200;
}
.card {
@apply bg-white rounded-md shadow-sm p-6 border border-primary-200 hover:shadow-md transition-shadow duration-200;
}
}
+20
View File
@@ -0,0 +1,20 @@
export interface PostData {
id: string;
title: string;
date: string;
excerpt: string;
content: string;
author: string;
tags: string[];
coverImage?: string;
}
export interface PostMetadata {
id: string;
title: string;
date: string;
excerpt: string;
author: string;
tags: string[];
coverImage?: string;
}
+43
View File
@@ -0,0 +1,43 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx}',
'./src/components/**/*.{js,ts,jsx,tsx}'
],
theme: {
extend: {
colors: {
primary: {
50: '#f8f9fa',
100: '#e9ecef',
200: '#dee2e6',
300: '#ced4da',
400: '#adb5bd',
500: '#6c757d',
600: '#495057',
700: '#343a40',
800: '#212529',
900: '#121212',
},
accent: {
light: '#e2d8c6',
DEFAULT: '#9d8c70',
dark: '#695c4a',
},
},
fontFamily: {
serif: ['Baskerville', 'Georgia', 'Times New Roman', 'serif'],
sans: ['Helvetica Neue', 'Arial', 'sans-serif'],
},
typography: {
DEFAULT: {
css: {
'blockquote p:first-of-type::before': false,
'blockquote p:last-of-type::after': false,
},
},
},
},
},
plugins: [require('@tailwindcss/typography')],
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}