diff --git a/.env.example b/.env.example index 6004a7d..e9111fe 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,6 @@ -# TinaCMS — Copy this file to .env and fill in your values. -# Values are generated at app.tina.io after connecting your GitHub repo. -# -# IMPORTANT: Tina reads .env (not .env.local) during tinacms build. -# For local development you can put values in .env.local too, but -# for Vercel add them directly in the project's Environment Variables. - -NEXT_PUBLIC_TINA_CLIENT_ID= -TINA_TOKEN= +# Canonical site URL (metadataBase, Open Graph absolute URLs, sitemap). Set in production / Vercel. +# Example: https://www.worldfishdigital.org +# NEXT_PUBLIC_SITE_URL=https://www.example.org # Automatically set by Vercel — no action needed: # NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF= diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index ea782d7..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..adb1733 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +jobs: + lint-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + - run: npm ci + - run: npm run lint + - run: npm run build diff --git a/.gitignore b/.gitignore index 73c71af..0f4f4e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -package-lock.json yarn.lock # dependencies @@ -36,10 +35,6 @@ yarn-error.log* # vercel .vercel -# TinaCMS — generated at build time, do not commit -/public/admin -/tina/__generated__ - #vscode settings .vscode diff --git a/README.md b/README.md index 2d29df2..db7e0f0 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,32 @@ -# Peskas.show +# WorldFish Digital -Official website for Peskas™ - An open-source digital platform for small-scale fisheries. +Main site for [WorldFish](https://www.worldfishcenter.org/) digital tools and platforms—including **Peskas™** and related products—news, and resources. -## Tech Stack +## Run locally -- **Framework**: [Next.js 16.1.1](https://nextjs.org/) with App Router -- **React**: 19.0.0 -- **Styling**: SCSS/CSS with Bootstrap-based theme -- **Content**: Markdown-based blog system -- **Deployment**: Vercel +You need **Node.js 20+** (see `.nvmrc`). -## Getting Started - -### Prerequisites - -- Node.js >= 20.9.0 (see `.nvmrc`) -- npm or yarn - -### Installation - -1. Clone the repository -2. Install dependencies: - ```bash - npm install - ``` - -3. Copy environment variables: - ```bash - cp .env.example .env.local - ``` - Edit `.env.local` with your configuration if needed. - -4. Run the development server: - ```bash - npm run dev - ``` - -5. Open [http://localhost:3000](http://localhost:3000) in your browser. - -## Project Structure - -``` -peskas.show/ -├── app/ # Next.js App Router pages -│ ├── blog/ # Blog listing and individual posts -│ ├── data-resources/ # Data resources page -│ ├── how-it-works/ # How it works page -│ ├── layout.js # Root layout -│ ├── page.js # Homepage -│ ├── sitemap.js # Dynamic sitemap generation -│ └── robots.js # Robots.txt generation -├── components/ # React components -│ ├── elements/ # Reusable UI elements -│ ├── layout/ # Layout components (Header, Footer, etc.) -│ └── sections/ # Page sections -├── lib/ # Utility functions -│ ├── posts.js # Blog post utilities -│ └── constants.js # Application constants -├── posts/ # Markdown blog posts -├── public/ # Static assets -└── styles/ # Global styles +```bash +npm install +cp .env.example .env.local # optional; set your public site URL for previews +npm run dev ``` -## Adding Blog Posts - -1. Create a new `.md` file in the `posts/` directory -2. Add frontmatter with required fields: - ```yaml - --- - title: "Your Post Title" - author: "Author Name" - date: "2024-01-01" - draft: false - description: "Post description" - tags: - - tag1 - - tag2 - coverImage: "/assets/imgs/page/blog/image.jpg" - --- - ``` -3. Write your content below the frontmatter -4. Posts with `draft: true` will not appear in production - -See `docs/BLOG_SETUP.md` for more details. - -## Available Scripts +Open [http://localhost:3000](http://localhost:3000). -- `npm run dev` - Start development server -- `npm run build` - Build for production -- `npm run start` - Start production server -- `npm run lint` - Run ESLint -- `npm run sass` - Watch and compile SCSS (if needed) +## Blog posts -## Deployment +Add Markdown files under `posts/`. Use frontmatter for title, date, `draft` (hide when `true`), and optionally `channel` (`worldfish` or `peskas`). Images and assets live under `public/`. -This project is configured for deployment on Vercel. See `docs/VERCEL_SETUP.md` for deployment details. +## Production build -### Environment Variables - -Configure the following in your deployment platform: - -- `NODE_ENV` - Set to `production` for production builds -- `NEXT_PUBLIC_SITE_URL` - Full URL of your site (e.g., `https://peskas.show`) - -See `.env.example` for all available environment variables. - -## Features - -- ✅ Modern Next.js App Router architecture -- ✅ Server-side rendering (SSR) and static site generation (SSG) -- ✅ Markdown-based blog system -- ✅ SEO optimized (sitemap, robots.txt, metadata) -- ✅ Error boundaries for graceful error handling -- ✅ Responsive design -- ✅ Performance optimized - -## Development - -### Code Style - -- ESLint is configured with Next.js recommended rules -- Prettier is configured for consistent code formatting -- Use 4-space indentation (as per Prettier config) - -### Key Features - -- **Error Boundaries**: Graceful error handling in production -- **Constants**: Centralized configuration in `lib/constants.js` -- **Type Safety**: JSDoc comments for better IDE support (TypeScript migration optional) - -## Learn More +```bash +npm run build +npm start +``` -- [Next.js Documentation](https://nextjs.org/docs) -- [React Documentation](https://react.dev) -- [Next.js App Router](https://nextjs.org/docs/app) +Deploy however you host Node apps; many teams use **Vercel**. Set **`NEXT_PUBLIC_SITE_URL`** to your live site URL so links and social previews resolve correctly. ## License -See LICENSE file for details. +See `LICENSE`. diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 419b787..3eac1ce 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -1,14 +1,20 @@ import BlogPostClient from "@/components/sections/BlogPostClient"; import { getPostBySlug, getAllPostSlugs } from "@/lib/posts"; import { notFound } from "next/navigation"; +import { POST_CHANNELS, WORLD_FISH_SITE, PESKAS_PRODUCT } from "@/lib/constants"; export async function generateMetadata({ params }) { const { slug } = await params; const post = getPostBySlug(slug); - if (!post) return { title: 'Post Not Found' }; + if (!post || post.draft) return { title: 'Post Not Found' }; + + const titleSuffix = + post.channel === POST_CHANNELS.peskas + ? `${PESKAS_PRODUCT.name} — ${WORLD_FISH_SITE.name}` + : WORLD_FISH_SITE.name; return { - title: `${post.title} - Peskas Blog`, + title: `${post.title} | ${titleSuffix}`, description: post.description || post.content?.substring(0, 160), authors: post.author ? [{ name: post.author }] : [], robots: { @@ -30,8 +36,8 @@ export async function generateStaticParams() { export default async function BlogPost({ params }) { const { slug } = await params; const post = getPostBySlug(slug); - - if (!post) { + + if (!post || post.draft) { notFound(); } diff --git a/app/blog/page.js b/app/blog/page.js index 542a398..92a5db8 100644 --- a/app/blog/page.js +++ b/app/blog/page.js @@ -1,11 +1,12 @@ -import Link from "next/link"; -import Layout from "@/components/layout/Layout"; -import { getAllPosts } from "@/lib/posts"; -import { BLOG_CONFIG } from "@/lib/constants"; +import Link from 'next/link'; +import Layout from '@/components/layout/Layout'; +import BlogCoverImage from '@/components/content/BlogCoverImage'; +import { getAllPosts } from '@/lib/posts'; +import { BLOG_WORLD_FISH, WORLD_FISH_SITE } from '@/lib/constants'; export const metadata = { - title: `${BLOG_CONFIG.title} - ${BLOG_CONFIG.description}`, - description: BLOG_CONFIG.description, + title: `${BLOG_WORLD_FISH.title} | ${WORLD_FISH_SITE.name}`, + description: BLOG_WORLD_FISH.description, robots: { index: true, follow: true, @@ -18,30 +19,27 @@ export const metadata = { export default async function Blog() { const posts = getAllPosts(); - + return ( -
-
-
-
-
-

{BLOG_CONFIG.title}

-

- {BLOG_CONFIG.description} -

-
+
+
+
+
+

{BLOG_WORLD_FISH.title}

+

{BLOG_WORLD_FISH.description}

-
-
+
+
{posts.length === 0 ? (
-

- No blog posts yet. Add markdown files to the posts/ directory to get started. +

+ No blog posts yet. Add markdown files to the{' '} + posts/ directory to get started.

@@ -55,7 +53,7 @@ export default async function Blog() { {new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', - day: 'numeric' + day: 'numeric', })} )} @@ -63,20 +61,15 @@ export default async function Blog() { {post.title} {post.description && ( -

- {post.description} -

+

{post.description}

)} {post.coverImage && (
- {post.title}
diff --git a/app/blog/peskas/page.js b/app/blog/peskas/page.js new file mode 100644 index 0000000..00cf109 --- /dev/null +++ b/app/blog/peskas/page.js @@ -0,0 +1,86 @@ +import Link from 'next/link'; +import Layout from '@/components/layout/Layout'; +import BlogCoverImage from '@/components/content/BlogCoverImage'; +import { getPostsByChannel } from '@/lib/posts'; +import { BLOG_PESKAS, WORLD_FISH_SITE } from '@/lib/constants'; + +export const metadata = { + title: `${BLOG_PESKAS.title} | ${WORLD_FISH_SITE.name}`, + description: BLOG_PESKAS.description, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + }, + }, +}; + +export default async function BlogPeskas() { + const posts = getPostsByChannel('peskas'); + + return ( + +
+
+
+
+

{BLOG_PESKAS.title}

+

{BLOG_PESKAS.description}

+
+
+
+
+
+
+ {posts.length === 0 ? ( +
+
+

+ No Peskas posts yet. Assign channel: peskas in{' '} + posts/*.md frontmatter. +

+
+
+ ) : ( +
+ {posts.map((post) => ( +
+
+ {post.date && ( + + {new Date(post.date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + + )} + + {post.title} + + {post.description && ( +

{post.description}

+ )} + {post.coverImage && ( +
+ + + +
+ )} +
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/app/data-resources/page.js b/app/data-resources/page.js index d8f6078..6567eba 100644 --- a/app/data-resources/page.js +++ b/app/data-resources/page.js @@ -1,8 +1,8 @@ 'use client' import { useEffect, useRef } from 'react'; -import Layout from "@/components/layout/Layout"; -import RichText from "@/components/content/RichText"; -import data from "@/content/pages/data-resources.json"; +import Layout from '@/components/layout/Layout'; +import RichText from '@/components/content/RichText'; +import data from '@/content/pages/data-resources.json'; export default function DataResources() { const dataverseRef = useRef(null); @@ -25,7 +25,8 @@ export default function DataResources() { script.onerror = () => { console.error('Failed to load Dataverse widget'); if (currentRef) { - currentRef.innerHTML = '

Unable to load Dataverse widget. Please try refreshing the page.

'; + currentRef.innerHTML = + '

Unable to load Dataverse widget. Please try refreshing the page.

'; } }; @@ -40,70 +41,48 @@ export default function DataResources() { }, []); return ( - <> - -
-
-
-
-
-

{data.hero.title}

- -
-
+ +
+
+
+
+

{data.hero.title}

+
-
-
-
-

{data.publications.title}

-
-
- -
+
+
+ +
+
+

{data.publications.title}

+
+
+
-
-
- -
-
-

{data.data.title}

-
-
- -
-
-
-
-
+