Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved blogging platform to use public repo #29

Merged
merged 2 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.local.encrypted
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e3db010b2ba38157507757a646313d99-640e1634a1f60c6350f00f10d340ec04b8beb40d8e6a22a09860359d10117e46ea6b536040d1b45f0eb2c2029a72ce3dc9179be447146f8b1b5ee1cc3e5ca0bcb75eafd814f3d768fed262afd7408104f76b7bec08c514a0c5da74001a497da831f7221be5c9b08981bbe0083036c8f408fa93d135f8e706164578a5c9cc82d4474f8577a35b78e176e8e34d8ab027b3525a4e78f71b160e7697a3937ddfe91458acafbe8761c0fa2c2e8198007a24e7fc0ab42fb66f12a308e357d31cd9c5ba285bb08dbaf1d65927054b053f14413f77f40e51f232cafc4343feba85175e3f
69773f4ab32c8c448b11bdc54da2c4fd-2e3474c171b89de539c0c8e0054a9ca3da02669bc7434f394b258b30d3c1606e42042b913ace46cc060b8523dea43bace50168c188d8706efeb80d53b3a71eaea7e10a79b00a83906716b0d31e9b3e81b0fc1c8debf43496343ced9c434cc927e10849fde0143106c7f2aa4b91eb619a616903a460f9061c8a673f7ce45b67af5ee3c721a0f62942cabfcd51452725ae553cc0f46cae4208437cdcb661357fda4cc9760096fd6b19d30b30aa45cd35eb4b9ac8520643d6f565ae87e8fa091558
2 changes: 1 addition & 1 deletion .env.production.encrypted
Original file line number Diff line number Diff line change
@@ -1 +1 @@
92c4a7b0062523ddfbb3d9b5422887c8-9e2335b9db88981c443d0d1e1951d17accf92f4f66c195f6c77282a4c008edd20238a1bd26f22f1ef56f025e9c5a785bc14f02bbda865b0d313b3cd87d6ac46650ae1291eed16c691d8a2c5fe64f5c337a531c14eaee0a14d59de341448c05f97b806df66893757f6967fdc2fe2f0fa8911b22add5fa13464292483d0d15fe8916412dae0d52e3ddb854f7415ceac7687562e8ee3747da3b4cf4f469aca1f62a00bc409593036bbc8cee287102d9928f725395da13e74964038c04e95fe89c7b783203eb39268a95b00ca73218800c8a061c0e9f2e296eda48cb8c47f8fdb89e2a6e514a6b27ec9e3c718efcb88485b9
ecbba554e0136e4a9e454a4a8c9832e0-83e61ff988e7ead98f69747ce662f13b74f4ae1308cf723160bd747b3d318c5c9a0630fa96c23726162b624621abf9868b2e4dcbcd5af8c172d76deada94479ed49ea04b02ee87c8f779df09f7e1591dfaaf9b15c349aad8b1f80fafb125ffc39d7468eac5dcec69e79e8ab686da6db44b84b960eb40da147d95897bb25942e639ea9e32752c83bb21d161293d0c34fe52168c703024de24f6ffb0cbcab3cc930e823bb6e02fa25cd19d156e5c0d5b299dec4c861ef74f800699158deb756abe
2 changes: 2 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const nextConfig = {
{ hostname: 'lh3.googleusercontent.com' },
{ hostname: '*.optimisticoder.com' },
{ hostname: 'api.dicebear.com' },
{ hostname: 'raw.githubusercontent.com' },
{ hostname: 'github.com' },
],
},
};
Expand Down
187 changes: 0 additions & 187 deletions scripts/prebuild.ts
Original file line number Diff line number Diff line change
@@ -1,196 +1,9 @@
import { execSync } from 'child_process';
import 'dotenv/config';
import fs from 'fs-extra';
import path from 'path';
import matter from 'gray-matter';
import type { MetadataRoute } from 'next';
import type { CategoryIndexEntry, StoriesIndexEntry } from '@/types/common';
import readingTime from 'reading-time';
import { APP_ENV } from '@/config/common';
import { simpleGit } from 'simple-git';

const storiesDir = '_stories';
const storiesIndexerPath = path.join(
process.cwd(),
storiesDir,
'index-stories.json',
);
const storiesMetaPath = path.join(process.cwd(), storiesDir, 'index-meta.json');

const generatedSitemapFilePath = path.join(
process.cwd(),
'src',
'generated-sitemap.json',
);

async function linkAssets() {
if (!fs.pathExistsSync(`./public/assets`)) {
try {
fs.symlinkSync(path.join(process.cwd(), storiesDir), './public/assets');
console.info('Successfuly link stories folder.');
} catch (error) {
console.error((error as Error).message);
}
} else {
console.info('No link necessary to the stories folder.');
}
}

async function prepareStoriesFolder() {
const STORIES_KEY = process.env.STORIES_KEY;
const STORIES_REPO = process.env.STORIES_REPO;
const gitUrl = `https://oauth2:${STORIES_KEY}@github.com/${STORIES_REPO}.git`;
if (
!fs.pathExistsSync(`./${storiesDir}`) ||
fs.readdirSync(`./${storiesDir}`).length === 0
) {
try {
console.info("Stories folder doesn't exist, cloning...");
execSync(`git clone ${gitUrl} ${storiesDir}`);
if (APP_ENV !== 'production') {
execSync(`
cd ${storiesDir} &&
git checkout test`);
simpleGit(storiesDir).checkout('test');
console.info(
'Environment is not for production, checked in to test branch instead.',
);
}
console.info('Successfuly cloning stories folder.');
} catch (error) {
console.error('Failed to clone the repository.');
}
return;
}
try {
console.info('Stories folder already exist, syncing...');
execSync(`
cd ${storiesDir} &&
git fetch origin &&
git reset --hard origin/main &&
`);
if (APP_ENV !== 'production') {
execSync(`
cd ${storiesDir} &&
git checkout test`);
simpleGit(storiesDir).checkout('test');
console.info(
'Environment is not for production, checked in to test branch instead.',
);
}
simpleGit(storiesDir).pull();
console.info('Successfuly synced stories folder.');
} catch (error) {
console.error('Failed to sync the repository.');
}
return;
}

async function getFiles(dir: string, depth: number = 0): Promise<string[]> {
const subdirs = await fs.readdir(dir);
const files = await Promise.all(
subdirs.map(async (subdir) => {
const res = path.resolve(dir, subdir);
const stat = await fs.stat(res);

// Skip .git, node_modules, and all first-level files
if (
subdir === '.git' ||
subdir === 'node_modules' ||
(depth === 0 && stat.isFile())
) {
return [];
}

// Recursively get files from directories, filter out non-page.md files
if (stat.isDirectory()) {
return getFiles(res, depth + 1);
} else if (path.basename(res) === 'page.md') {
return [res];
} else {
return [];
}
}),
);

return files.reduce((a, f) => a.concat(f), []);
}

async function indexFiles() {
const files = await getFiles(storiesDir);
const storiesIndex: StoriesIndexEntry[] = [];
const categoriesIndex: CategoryIndexEntry = [];

for (const file of files) {
if (path.basename(file) === 'page.md') {
const content = await fs.readFile(file, 'utf8');
const { data } = matter(content);
const relativePath = path.relative(storiesDir, file);
const [category, slug] = relativePath.split(path.sep);

storiesIndex.push({
title: data.title,
slug: slug,
category: category,
subtitle: data.subtitle,
keywords: data.keywords,
excerpt: data.excerpt,
cover: data.cover,
date: data.date,
readTime: readingTime(content).text,
});
if (!categoriesIndex.includes(category)) {
categoriesIndex.push(category);
}
}
}
// Sort the index by date
storiesIndex.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);

fs.writeFileSync(
storiesMetaPath,
JSON.stringify(
{
totalStories: storiesIndex.length,
categories: categoriesIndex,
},
null,
2,
),
);
fs.writeFileSync(storiesIndexerPath, JSON.stringify(storiesIndex, null, 2));
console.info('Indexing complete. Check index file for results.');
}

async function generateSitemapXml() {
const sitemapEntries: MetadataRoute.Sitemap = [];
const storiesIndexes: StoriesIndexEntry[] = JSON.parse(
fs.readFileSync(storiesIndexerPath).toString(),
);
storiesIndexes.forEach((item) => {
sitemapEntries.push({
url: `https://optimisticoder.com/stories/${item.slug}`,
lastModified: item.date,
changeFrequency: 'weekly',
priority: 0.5,
});
});
fs.writeFileSync(
generatedSitemapFilePath,
JSON.stringify(sitemapEntries, null, 2),
);
console.info('Sitemap successfully generated.');
}

async function main() {
const APP_ENV = process.env.NEXT_PUBLIC_APP_ENV ?? 'local';
try {
await prepareStoriesFolder();
await indexFiles();
await linkAssets();
await generateSitemapXml();
if (APP_ENV === 'local') {
return;
}
Expand Down
8 changes: 6 additions & 2 deletions src/app/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,14 @@ function MostHighlightedStory({ story }: { story?: StoriesIndexEntry }) {
className="px-8 md:px-32 py-16"
>
<div className="border-[#909090] border shadow-primary glowing relative bg-light dark:glowing-dark dark:bg-[#1f201f] dark:border-[#5d5d5d] rounded-lg overflow-hidden gap-4 grid lg:grid-cols-2">
<Link href={`/stories/${story.slug}`} title={`Read ${story.title}`}>
<Link
href={`/stories/${story.slug}`}
className="aspect-[4/3] overflow-hidden border-[#909090] lg:border-r border-b dark:border-[#5d5d5d]"
title={`Read ${story.title}`}
>
<FallbackImage
src={story.cover}
className="w-full h-full object-cover aspect-[4/3] border-[#909090] lg:border-r border-b dark:border-[#5d5d5d]"
className="w-full hover:scale-105 transition-transform duration-500 h-full object-cover aspect-[4/3]"
alt={`Thumbnail of ${story.title}`}
width={512}
height={256}
Expand Down
25 changes: 15 additions & 10 deletions src/app/disclaimer/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export const revalidate = 604800;
import { Breadcrumbs, MarkdownUI, StoryTitle } from '@/components/common';
import { readFileSync } from 'fs';
import { STORIES_URL } from '@/config/common';
import { sfetch } from '@/helpers/common';
import type { PlainTextResponse } from '@/types/responses';
import matter from 'gray-matter';
import type { Metadata, ResolvingMetadata } from 'next';
import { notFound } from 'next/navigation';
import path from 'path';
import { cache } from 'react';

type Props = {
Expand All @@ -13,23 +14,27 @@ type Props = {

const getDisclaimer = cache(async () => {
try {
const { content: markdown, data } = matter(
readFileSync(
path.join(process.cwd(), './src', 'markdowns', 'disclaimer.md'),
'utf8',
),
);
const res = (await sfetch(`${STORIES_URL}/docs/disclaimer.md`, {
next: {
revalidate: 604800,
},
})) as PlainTextResponse;
if (!res.ok) {
throw new Error((await res.json()).message);
}

const { content: markdown, data } = matter(await res.text());
return {
content: markdown.toString(),
title: data.title,
subtitle: `Updated ${new Date(data.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
month: 'long',
day: 'numeric',
})}.`,
};
} catch (error) {
notFound();
return notFound();
}
});

Expand Down
23 changes: 15 additions & 8 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@ import {
LatestStories,
MostHighlightedStory,
} from '@/app/components';
import { STORIES_DIR } from '@/config/common';
import { STORIES_URL } from '@/config/common';
import { sfetch } from '@/helpers/common';
import type { StoriesIndexEntry } from '@/types/common';
import { readFileSync } from 'fs';
import type { CommonResponse } from '@/types/responses';
import jsonata from 'jsonata';
import path from 'path';

async function listFiveLatestStories() {
try {
const indexStories: StoriesIndexEntry[] = JSON.parse(
readFileSync(
path.join(process.cwd(), STORIES_DIR, 'index-stories.json'),
).toString(),
);
const res = (await sfetch(`${STORIES_URL}/stories/stories.json`, {
next: {
revalidate: 86400,
tags: ['stories'],
},
})) as CommonResponse<StoriesIndexEntry[]>;
if (!res.ok) {
throw new Error((await res.json()).message);
}
const indexStories = (await res.json()).map((story) => {
return { ...story, cover: `${STORIES_URL}/stories/${story.cover}` };
});
const expression = jsonata(`$[]`);
const result: StoriesIndexEntry[] = await expression.evaluate(indexStories);
return result.slice(0, 5);
Expand Down
25 changes: 15 additions & 10 deletions src/app/privacy-policy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export const revalidate = 604800;
import { Breadcrumbs, MarkdownUI, StoryTitle } from '@/components/common';
import { readFileSync } from 'fs';
import { STORIES_URL } from '@/config/common';
import { sfetch } from '@/helpers/common';
import type { PlainTextResponse } from '@/types/responses';
import matter from 'gray-matter';
import type { Metadata, ResolvingMetadata } from 'next';
import { notFound } from 'next/navigation';
import path from 'path';
import { cache } from 'react';

type Props = {
Expand All @@ -13,23 +14,27 @@ type Props = {

const getPrivacyPolicy = cache(async () => {
try {
const { content: markdown, data } = matter(
readFileSync(
path.join(process.cwd(), './src', 'markdowns', 'privacy-policy.md'),
'utf8',
),
);
const res = (await sfetch(`${STORIES_URL}/docs/privacy-policy.md`, {
next: {
revalidate: 604800,
},
})) as PlainTextResponse;
if (!res.ok) {
throw new Error((await res.json()).message);
}

const { content: markdown, data } = matter(await res.text());
return {
content: markdown.toString(),
title: data.title,
subtitle: `Updated ${new Date(data.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
month: 'long',
day: 'numeric',
})}.`,
};
} catch (error) {
notFound();
return notFound();
}
});

Expand Down
Loading
Loading