{"id":23086961,"url":"https://github.com/chaseottofy/nextjs-blog","last_synced_at":"2025-04-30T19:29:17.062Z","repository":{"id":192402999,"uuid":"686184686","full_name":"chaseottofy/nextjs-blog","owner":"chaseottofy","description":"nextjs blog","archived":false,"fork":false,"pushed_at":"2023-10-27T15:25:32.000Z","size":5079,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-30T19:12:51.026Z","etag":null,"topics":["blog","contentlayer","nextjs","react","typescript"],"latest_commit_sha":null,"homepage":"https://nextjs-blog-ottofy.vercel.app","language":"MDX","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chaseottofy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-02T00:56:47.000Z","updated_at":"2024-05-24T04:27:28.000Z","dependencies_parsed_at":null,"dependency_job_id":"e7f83f6a-b583-4c16-9cf7-6376811cdffb","html_url":"https://github.com/chaseottofy/nextjs-blog","commit_stats":null,"previous_names":["chaseottofy/nextjs-blog"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chaseottofy%2Fnextjs-blog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chaseottofy%2Fnextjs-blog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chaseottofy%2Fnextjs-blog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chaseottofy%2Fnextjs-blog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chaseottofy","download_url":"https://codeload.github.com/chaseottofy/nextjs-blog/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251768943,"owners_count":21640802,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blog","contentlayer","nextjs","react","typescript"],"created_at":"2024-12-16T19:39:44.800Z","updated_at":"2025-04-30T19:29:17.036Z","avatar_url":"https://github.com/chaseottofy.png","language":"MDX","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Next.js Blog with Contentlayer\n\n\u003e None of the posts are mine\n\n\u003cimg src=\"screenshots/screen3.jpg\" width=\"700\"\u003e\n\n---\n\n## Table of Contents\n\n* [Features](#features)\n  + [Image Handling](#custom-image-handling-system)\n  + [Accessibility](#accessibility)\n  + [Contentlayer](#contentlayer)\n  + [Next.js](#nextjs)\n  + [CSS Modules](#css-modules)\n  + [TypeScript](#typescript)\n  + [Optimized](#optimized)\n* [Installation](#installation)\n* [MDX](#mdx)\n  + [MDX Schema](#mdx-schema)\n  + [Featured Posts](#featured-posts)\n  + [Supplying App with Posts](#supplying-app-with-posts)\n* [Images](#images)\n* [Types \u0026 Interfaces](#types--interfaces)\n* [Plugins](#plugins)\n* [Fonts](#fonts)\n* [CSS](#css)\n* [Linting](#linting)\n* [License](#license)\n\n---\n\n## Features\n\n#### Custom Image Handling System\n\n* Automatically generate base64 data urls and placeholders for all images in specified folder.\n* Large banners are cached, served as webp, lazy loaded when appropriate, have fallbacks, and cached when possible.\n\n#### Accessibility\n\nPasses the following audits:\n\n* lighthouse performance/a11y/seo/best practices (100%)\n* NU HTML Checker (100%)\n* PageSpeed Insights audit (100%)\n* WCAG 2.1 AA/AAA contrast ratio requirements (100%)\n\n#### Contentlayer\n\n* Makes for an easy CMS-like experience for posts.\n* MDX -\u003e JSON -\u003e TypeScript\n* Custom table of content generator for posts.\n\n#### Next.js\n\n* Utilizes the latest Next.js features\n  + New `/app` directory\n\n#### CSS Modules\n\n* Aside from resets, variables, and global styles, all CSS is modularized and scoped to the component it's used in.\n\n#### TypeScript\n\n* 100% TypeScript with predominate use of interfaces.\n\n#### Optimized\n\n* 100% lighthouse score\n* Tested with disabled cache and simulated base throttling\n* 0.0s cumulative layout shift across all pages\n\n\u003e **Note**\n\u003e 9/13 - this screenshot is outdated, need to fix SEO.\n\n\u003cimg src=\"screenshots/screen_lh1.jpg\"\u003e\n\n## MDX\n\n* MDX is a superset of markdown that allows you to use JSX components inside of markdown.\n* Used in coherence with `contentlayer` and `next-contentlayer` to create a CMS-like experience.\n\nAll MDX files are located in the `@/posts` folder\n\n### MDX Schema\n\nAt the top of each MDX file is a list of properties that are used to create the post.\n* They are 100% customizable and are typed in the `contentlayer.config.ts` file in the root directory.\n* My example schema is below, make sure to add the `---` at the top and bottom of the schema without any spaces.\n* Note that whitespace is important.\n\n| Property | Type | Required | Description |\n| --- | --- | --- | --- |\n| title | string | true | title of post |\n| date | string | true | ISO 'YYYY-MM-DDTHH: MM: SS' |\n| author | string | true | author name |\n| authorLink | string | true | author website |\n| excerpt | string | true | short description of post |\n| banner | string | false | direct path to image file |\n| isFeatured | boolean | false | featured posts will have their banner image displayed on the home page |\n| isArchived | boolean | false | archived posts will not be displayed |\n| tags | string[] | true | list of tags |\n\n```MDX\n---\ntitle: All examples written by CHATGPT\ndate: '2023-08-10T11:30:30'\nauthor: otto\nauthorLink: https://ottofy.dev\nexcerpt: lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua\nbanner: '/images/posts/post-01.webp'\nisFeatured: true\nisArchived: false\ntags: \n  - javascript\n  - react\n---\n```\n\n### Table of Contents\n\nThe table of contents is generated automatically from the headings in the post.\n\nThe generation follows the following rules:\n\n* Only headings \n\n### Featured Posts\n\n* contentlayer param featured (boolean)\n\nFeatured posts will have their banner image displayed on the home page.\n`@app/layout.tsx` - If you would also like to display the featured posts first on the home page, set the second parameter of `getPostsSorted` to `true` .\n\n```JSX\nconst startPosts = getPostsSorted('asc', true);\n```\n\n* Featured posts have one additional image property to be aware of: `banner`\n\n`banner` : Direct path to the image file i.e. `banner: '/images/featured-post.jpg'`\n\n### Supplying App with Posts\n\nCalls to post data must be made within client-side components. I chose to do this in my very outermost client component, `@app/layout.tsx` . By doing this, I only ever make one call to retrieve the posts.\n\n* `children` represents the top-level of all components that directly descend from `\u003cmain\u003e`.\n* In order to pass the posts to all children, a singular time, I added the parameter `params` and explicitly added a prop to attach the posts to it.\n* `\u003cHeader\u003e` and `{children}` receive the same posts, one time.\n\n```JSX\nconst RootLayout: React.FC\u003cRootLayoutInterface\u003e = ({\n  children,\n  params,\n}) =\u003e {\n  const startPosts = getPostsSorted('asc', true);\n  params.startPosts = startPosts;\n}\n\nreturn (\n  \u003chtml\u003e\n    \u003cbody\u003e\n      \u003cThemeProvider\u003e\n        \u003cHeader posts={startPosts} /\u003e\n        \u003cmain\u003e{children}\u003c/main\u003e\n        \u003cFooter /\u003e\n      \u003c/ThemeProvider\u003e\n    \u003c/body\u003e\n  \u003c/html\u003e\n);\n```\n\n## Images\n\n**All images are in the `public/images` folder.**\n\n\u003e **Warning**\n\u003e Before using the image parser, make sure to configure the `ImageConfig` class within the `scripts/img-parser.mjs` file to match your images folder structure and desired output schema.\n\n* Within the `scripts` folder is a useful node script that generates placeholder images from a specified folder to a specified placeholder folder.\n* This script also can generate base64 data urls for the placeholder images, and output them as an object in either a typescript, json, or mdx file.\n* The generated base64 string can be used in the `placeholder` prop of the `\u003cImage\u003e` component in nextjs to prevent screen flashing when loading images. Nextjs has a built in feature to easily incorporate this technique by simply supplying the base64 data url under the `placeholder` prop of the `\u003cImage\u003e` component. The generated base64 data is cached directly in memory and is often around 300 bytes.\n\nThe script is located in `scripts/img-parser.mjs` - The following CLI commands are available:\n\n1. Create both placeholder images and base64 data urls:\n\n```bash\nnpm run parseimg\n```\n\n2. Create placeholder images only:\n\n```bash\nnpm run parseimg:placeholders\n```\n\n3. Create base64 data urls from placeholders only:\n\n```bash\nnpm run parseimg:base64\n```\n\nAn example of the generated base64 data url object is below. The key is name of the original image file, and the value is the base64 data url of the placeholder image for that file. The outputted object itself will be in a generated file of specified type and location.\n\n```TS\nconst imagePlaceholders = {\n  \"post-01\": \"data:image/webp;base64,UklGRqAAAABXRUJQVlA4IJQAAABQBgCdASp4ADQAP83i6W8/tjGuJBQMS/A5iWMAy6QANzVkMe/6zQl6bimj+ABd/WW1rl03B5JW6htFAADkbp+DjCar5D8jsIbsKGD+2dih3ULc2kxX+7nALoJEgZ6YUqv4M84JNd3d8sjBNrkFg+FCfVf14QM54Y0k3SjL1loua3I/yhu2fdkCSTWwSNSeAAS4AAAA\",\n  \"post-02\": \"data:image/webp;base64,UklGRu4AAABXRUJQVlA4IOIAAAAQCQCdASrAAFMAP83m63I/t7+/oIpD8DmJZW7dXOCAAuWnctTTwgBi/CEliZNRdPeeVs9YJUQjuDZwfcbseXJJOJl/JTBPhd5c6uKRd6ITVAAA5Fd583sj+BfjCar5D+R2EN2GbgPxIb+rwVEAD00qlBlcqxNyE7LAdQ37q3R4UQvaeEtNVIQo3MUgygOJw9a1fq3s1NkotEv37bwHGx23s6U+FYGeLkwJ4fgU/pmBFgMs0GyYnoGyCd4LdLHD51nSEjpNJpR7HAghj5veaC6EkVM2cKxkCZzXhAwqeCCAAAAA\"\n};\nexport default imagePlaceholders;\n```\n\nTo retrieve the placeholder, simply import the object and use the post slug as the key.\n\n```JSX\nimport imagePlaceholders from '@/data/image-placeholder.ts';\nconst postImagePlaceholder = imagePlaceholders[post.paramsAsSlug];\n```\n\nThen supply the placeholder to the `\u003cImage\u003e` component.\n\n```JSX\n\u003cImage \n  src={post.banner}\n  placeholder={postImagePlaceholder}\n/\u003e\n```\n\nOr alternatively, use it both as the placeholder and as a content fallback.\n\n```JSX\n\u003cImage \n  src={post?.banner ? post.banner : placeholderImageSrc}\n  placeholder={postImagePlaceholder}\n/\u003e\n```\n\nBelow is an example of the difference it can make. This becomes more noticeable when caching is disabled.\n\n\u003cimg src=\"screenshots/pi_tech.gif\"\u003e\n\n---\n\n## Types \u0026 Interfaces\n\n* All types and interfaces used more than once are in the @/models/interfaces.ts file.\n* All types and interfaces used only once are in the file they are used in.\n\n## Plugins\n\n* `contentlayer` : Bridge between MDX and TypeScript\n* `next-contentlayer` : Next.js plugin for contentlayer\n* `sharp` : Image processing ( no need to do anything with this one next.js automatically uses it )\n* `next-themes` : Theme provider for Next.js\n\n## Fonts\n\n* The main font is called Neue Montreal from [here](https://pangrampangram.com/products/neue-montreal)\n\n* The font used for titles is called `Grotesque`\n  + I got it from [basement.studio](https://grotesque.basement.studio/) - they have a 'tweet to download' system but I don't have social media so I just ripped it from the site lol.\n\n## CSS\n\n* 90% of the CSS is modularized and scoped to the component it's used in.\n* The only global styles, aside from variables and resets, are those relative to the blog content itself which are located in [styles/mdx.css](https://github.com/chaseottofy/nextjs-blog/blob/main/styles/mdx.css)\n\n## Linting\n\n* I always use an obnoxious linting config for development, depending on whether I'm finished with this project by the time you're reading this, you may want to tone it down.\n\n## License\n\nMIT License\n\nCopyright (c) 2023 Chase Ottofy\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, \nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchaseottofy%2Fnextjs-blog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchaseottofy%2Fnextjs-blog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchaseottofy%2Fnextjs-blog/lists"}