├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .github ├── nodejs.version └── workflows │ ├── check.yml │ ├── cr.yml │ ├── nextjs_bundle_analysis.yml │ └── playwright.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc ├── .releaserc ├── .storybook ├── main.ts └── preview.ts ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── codegen.ts ├── git-conventional-commits.yaml ├── graph.svg ├── i18n.ts ├── instrumentation.ts ├── jest.config.mjs ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.mjs ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── project-screenshot.png ├── public ├── favicon.ico └── icons │ ├── X.svg │ ├── facebook.svg │ ├── instagram.svg │ └── youtube.svg ├── renovate.json ├── reset.d.ts ├── src ├── app │ ├── (sitemaps) │ │ ├── [lang] │ │ │ ├── article-sitemap │ │ │ │ └── [page] │ │ │ │ │ └── server-sitemap.xml │ │ │ │ │ └── route.ts │ │ │ └── sitemap.xml │ │ │ │ └── route.ts │ │ └── server-sitemap-index.xml │ │ │ └── route.ts │ ├── GoogleAnalytics.tsx │ ├── Providers.tsx │ ├── [lang] │ │ ├── [...rest] │ │ │ └── page.tsx │ │ ├── [slug] │ │ │ └── page.tsx │ │ ├── article │ │ │ └── [slug] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── category │ │ │ └── [slug] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ └── page.tsx │ └── api │ │ ├── [lang] │ │ ├── generateRssFile.ts │ │ └── route.ts │ │ ├── health │ │ └── route.ts │ │ ├── httpError.ts │ │ ├── og │ │ └── route.tsx │ │ ├── stockdaily │ │ └── route.ts │ │ └── webhook │ │ ├── algoliaClient.ts │ │ ├── handleRevalidation.ts │ │ ├── publish │ │ └── route.ts │ │ ├── unpublish │ │ └── route.ts │ │ ├── validateBody.ts │ │ └── validateSignature.ts ├── components │ ├── ArticleCard │ │ ├── ArticleCard.tsx │ │ ├── ArticleMinifiedCard.tsx │ │ ├── ArticlePublishDetails.tsx │ │ ├── Buttons │ │ │ ├── LiveButton.tsx │ │ │ ├── PlayButton.tsx │ │ │ └── Tag.tsx │ │ └── HeroArticleCard.tsx │ ├── ArticlesGrid │ │ └── ArticlesGrid.tsx │ ├── CategoryArticles │ │ ├── CategoryArticles.tsx │ │ ├── CategoryArticlesInfinite.tsx │ │ └── CategoryArticlesInfiniteDynamic.tsx │ ├── CodeSnippet │ │ ├── CodeSnippet.tsx │ │ └── CodeSnippetDynamic.tsx │ ├── Footer │ │ └── Footer.tsx │ ├── HighlightedArticles │ │ └── HighlightedArticles.tsx │ ├── HighlightedCategoryArticles │ │ └── HighlightedCategoryArticles.tsx │ ├── LangSelect │ │ ├── DynamicLangSelect.tsx │ │ └── LangSelect.tsx │ ├── Navigation │ │ └── Navigation.tsx │ ├── Quiz │ │ ├── QuizDynamic.tsx │ │ └── QuizLogic.tsx │ ├── RecentArticles │ │ ├── RecentArticles.tsx │ │ ├── RecentArticlesInfinite.tsx │ │ └── RecentArticlesInfiniteDynamic.tsx │ ├── RecommendedArticles │ │ └── RecommendedArticles.tsx │ ├── RichText │ │ └── RichText.tsx │ ├── Search │ │ ├── DynamicSearchDialog.tsx │ │ ├── RefinementCombobox.tsx │ │ └── SearchDialog.tsx │ ├── ShareOnSocial │ │ └── ShareOnSocial.tsx │ ├── StockDisplay │ │ ├── StockDisplay.tsx │ │ ├── StockDisplayRenderer.tsx │ │ └── StockQuote.tsx │ ├── TrendingArticles │ │ ├── TrendingArticles.tsx │ │ ├── getTrendingArticles.ts │ │ └── reportConfig.ts │ └── ui │ │ ├── Button │ │ ├── Button.stories.tsx │ │ └── Button.tsx │ │ ├── Checkbox │ │ └── Checkbox.tsx │ │ ├── Command │ │ └── Command.tsx │ │ ├── Dialog │ │ └── Dialog.tsx │ │ ├── Input │ │ └── Input.tsx │ │ ├── Popover │ │ └── Popover.tsx │ │ ├── Select │ │ └── Select.tsx │ │ ├── Sheet │ │ └── Sheet.tsx │ │ ├── Skeleton │ │ └── Skeleton.tsx │ │ └── Tooltip │ │ └── Tooltip.tsx ├── e2e │ └── example.spec.ts ├── env.mjs ├── gql │ └── .gitkeep ├── i18n │ ├── i18n.ts │ ├── setTranslations.ts │ └── useTranslations.tsx ├── lib │ ├── client.ts │ ├── queries │ │ ├── articles.ts │ │ ├── components.ts │ │ ├── pages.ts │ │ ├── quizes.ts │ │ └── translations.ts │ └── tags.ts ├── middleware.ts ├── styles │ └── tailwind.css └── utils │ ├── cn.ts │ ├── formatDate.ts │ ├── getMetadataObj.ts │ ├── pipe.ts │ └── slateToText.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "next-news", 3 | "projectOwner": "Blazity", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "Pierniki", 15 | "name": "Marcin Szczepaniak", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/35572075?v=4", 17 | "profile": "https://github.com/Pierniki", 18 | "contributions": [ 19 | "code" 20 | ] 21 | }, 22 | { 23 | "login": "Max-Mogilski", 24 | "name": "Max", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/42366462?v=4", 26 | "profile": "https://github.com/Max-Mogilski", 27 | "contributions": [ 28 | "code" 29 | ] 30 | }, 31 | { 32 | "login": "bmstefanski", 33 | "name": "Bart Stefanski", 34 | "avatar_url": "https://avatars.githubusercontent.com/u/28964599?v=4", 35 | "profile": "https://bstefanski.com/", 36 | "contributions": [ 37 | "next-enterprise-code" 38 | ] 39 | }, 40 | { 41 | "login": "jjablonski-it", 42 | "name": "Jakub Jabłoński", 43 | "avatar_url": "https://avatars.githubusercontent.com/u/51968772?v=4", 44 | "profile": "https://github.com/jjablonski-it", 45 | "contributions": [ 46 | "next-enterprise-infra" 47 | ] 48 | } 49 | ], 50 | "types": { 51 | "next-enterprise-code": { 52 | "symbol": "💻", 53 | "description": "(next-enterprise) code", 54 | "link": "https://github.com/Blazity/next-enterprise/commits?author=bmstefanski" 55 | }, 56 | "next-enterprise-infra": { 57 | "symbol": "🚇", 58 | "description": "(next-enterprise) infra", 59 | "link": "#infra-jjablonski-it" 60 | } 61 | }, 62 | "contributorsPerLine": 7, 63 | "linkToUsage": true 64 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const fs = require("fs") 3 | 4 | module.exports = { 5 | extends: [ 6 | "next", 7 | "prettier", 8 | "react-app", 9 | "react-app/jest", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:storybook/recommended", 12 | "plugin:tailwindcss/recommended", 13 | ], 14 | parserOptions: { 15 | babelOptions: { 16 | presets: [require.resolve("next/babel")], 17 | }, 18 | }, 19 | rules: { 20 | "testing-library/prefer-screen-queries": "off", 21 | "@next/next/no-html-link-for-pages": "off", 22 | "@typescript-eslint/no-unused-vars": [ 23 | "warn", 24 | { 25 | argsIgnorePattern: "^_", 26 | varsIgnorePattern: "^_", 27 | }, 28 | ], 29 | "sort-imports": [ 30 | "warn", 31 | { 32 | ignoreCase: true, 33 | ignoreDeclarationSort: true, 34 | }, 35 | ], 36 | "tailwindcss/classnames-order": "off", 37 | "import/order": [ 38 | 1, 39 | { 40 | groups: ["external", "builtin", "internal", "sibling", "parent", "index"], 41 | pathGroups: [ 42 | ...getDirectoriesToSort().map((singleDir) => ({ 43 | pattern: `${singleDir}/**`, 44 | group: "internal", 45 | })), 46 | { 47 | pattern: "env", 48 | group: "internal", 49 | }, 50 | { 51 | pattern: "theme", 52 | group: "internal", 53 | }, 54 | { 55 | pattern: "public/**", 56 | group: "internal", 57 | position: "after", 58 | }, 59 | ], 60 | pathGroupsExcludedImportTypes: ["internal"], 61 | alphabetize: { 62 | order: "asc", 63 | caseInsensitive: true, 64 | }, 65 | }, 66 | ], 67 | }, 68 | } 69 | 70 | function getDirectoriesToSort() { 71 | const ignoredSortingDirectories = [".git", ".next", ".vscode", "node_modules"] 72 | return getDirectories(process.cwd()).filter((f) => !ignoredSortingDirectories.includes(f)) 73 | } 74 | 75 | function getDirectories(path) { 76 | return fs.readdirSync(path).filter(function (file) { 77 | return fs.statSync(path + "/" + file).isDirectory() 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /.github/nodejs.version: -------------------------------------------------------------------------------- 1 | v18.15.0 -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | check: 13 | env: 14 | SKIP_ENV_VALIDATION: true 15 | 16 | SKIP_BUILD_PRODUCT_REDIRECTS: 1 17 | GA_MEASUREMENT_ID: "${{ secrets.GA_MEASUREMENT_ID }}" 18 | GA_PROPERTY_ID: "${{ secrets.GA_PROPERTY_ID }}" 19 | GA_BASE64_SERVICE_ACCOUNT: "${{ secrets.GA_BASE64_SERVICE_ACCOUNT }}" 20 | NEXT_PUBLIC_HYGRAPH_CONTENT_API_URL: "${{ secrets.NEXT_PUBLIC_HYGRAPH_CONTENT_API_URL }}" 21 | NEXT_PUBLIC_ALGOLIA_API_ID: "${{ secrets.NEXT_PUBLIC_ALGOLIA_API_ID }}" 22 | NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY: "${{ secrets.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY }}" 23 | name: Check 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - run: echo "node_version=$(cat .github/nodejs.version)" >> $GITHUB_ENV 28 | - name: "use node ${{ env.node_version }}" 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: "${{ env.node_version }}" 32 | - name: Cache node modules 33 | uses: actions/cache@v2 34 | with: 35 | path: ~/.yarn 36 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-yarn- 39 | # The line above should be indented to align with 'restore-keys: |' 40 | 41 | - name: Install dependencies 42 | run: yarn install --frozen-lockfile 43 | - name: Install Playwright Browsers 44 | run: yarn playwright install --with-deps 45 | - name: Lint check 46 | run: yarn lint 47 | - name: Format check 48 | run: yarn prettier 49 | - name: Unit & Integration tests 50 | run: yarn test 51 | - name: Smoke & Acceptance tests 52 | run: | 53 | yarn build-storybook --quiet 54 | npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ 55 | "npx http-server storybook-static --port 6006 --silent" \ 56 | "npx wait-on tcp:127.0.0.1:6006 && yarn test-storybook" 57 | -------------------------------------------------------------------------------- /.github/workflows/cr.yml: -------------------------------------------------------------------------------- 1 | name: Code Review 2 | permissions: 3 | contents: read 4 | pull-requests: write 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | jobs: 12 | test: 13 | env: 14 | SKIP_ENV_VALIDATION: true 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: anc95/ChatGPT-CodeReview@main 18 | env: 19 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 20 | OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}" 21 | -------------------------------------------------------------------------------- /.github/workflows/nextjs_bundle_analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Next.js Bundle Analysis" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | defaults: 13 | run: 14 | # change this if your nextjs app does not live at the root of the repo 15 | working-directory: ./ 16 | 17 | jobs: 18 | analyze: 19 | env: 20 | SKIP_ENV_VALIDATION: true 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: "18.x" 29 | 30 | - name: Install dependencies 31 | run: yarn install --frozen-lockfile 32 | 33 | - name: Restore next build 34 | uses: actions/cache@v3 35 | id: restore-build-cache 36 | env: 37 | cache-name: cache-next-build 38 | with: 39 | path: .next/cache 40 | # change this if you prefer a more strict cache 41 | key: ${{ runner.os }}-build-${{ env.cache-name }} 42 | 43 | - name: Build next.js app 44 | env: 45 | SKIP_ENV_VALIDATION: true 46 | SKIP_BUILD_PRODUCT_REDIRECTS: 1 47 | GA_MEASUREMENT_ID: "${{ secrets.GA_MEASUREMENT_ID }}" 48 | GA_PROPERTY_ID: "${{ secrets.GA_PROPERTY_ID }}" 49 | GA_BASE64_SERVICE_ACCOUNT: "${{ secrets.GA_BASE64_SERVICE_ACCOUNT }}" 50 | NEXT_PUBLIC_HYGRAPH_CONTENT_API_URL: "${{ secrets.NEXT_PUBLIC_HYGRAPH_CONTENT_API_URL }}" 51 | NEXT_PUBLIC_ALGOLIA_API_ID: "${{ secrets.NEXT_PUBLIC_ALGOLIA_API_ID }}" 52 | NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY: "${{ secrets.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY }}" 53 | # change this if your site requires a custom build command 54 | run: yarn build 55 | 56 | # Here's the first place where next-bundle-analysis' own script is used 57 | # This step pulls the raw bundle stats for the current bundle 58 | - name: Analyze bundle 59 | run: npx -p nextjs-bundle-analysis report 60 | 61 | - name: Upload bundle 62 | uses: actions/upload-artifact@v3 63 | with: 64 | name: bundle 65 | path: .next/analyze/__bundle_analysis.json 66 | 67 | - name: Download base branch bundle stats 68 | uses: dawidd6/action-download-artifact@v2 69 | if: success() && github.event.number 70 | with: 71 | workflow: nextjs_bundle_analysis.yml 72 | branch: ${{ github.event.pull_request.base.ref }} 73 | path: .next/analyze/base 74 | 75 | # And here's the second place - this runs after we have both the current and 76 | # base branch bundle stats, and will compare them to determine what changed. 77 | # There are two configurable arguments that come from package.json: 78 | # 79 | # - budget: optional, set a budget (bytes) against which size changes are measured 80 | # it's set to 350kb here by default, as informed by the following piece: 81 | # https://infrequently.org/2021/03/the-performance-inequality-gap/ 82 | # 83 | # - red-status-percentage: sets the percent size increase where you get a red 84 | # status indicator, defaults to 20% 85 | # 86 | # Either of these arguments can be changed or removed by editing the `nextBundleAnalysis` 87 | # entry in your package.json file. 88 | - name: Compare with base branch bundle 89 | if: success() && github.event.number 90 | run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare 91 | 92 | - name: Get comment body 93 | id: get-comment-body 94 | if: success() && github.event.number 95 | uses: actions/github-script@v6 96 | with: 97 | result-encoding: string 98 | script: | 99 | const fs = require('fs') 100 | const comment = fs.readFileSync('.next/analyze/__bundle_analysis_comment.txt', 'utf8') 101 | core.setOutput('body', comment) 102 | 103 | - name: Find Comment 104 | uses: peter-evans/find-comment@v2 105 | if: success() && github.event.number 106 | id: fc 107 | with: 108 | issue-number: ${{ github.event.number }} 109 | body-includes: "" 110 | 111 | - name: Create Comment 112 | uses: peter-evans/create-or-update-comment@v3 113 | if: success() && github.event.number && steps.fc.outputs.comment-id == 0 114 | with: 115 | issue-number: ${{ github.event.number }} 116 | body: ${{ steps.get-comment-body.outputs.body }} 117 | 118 | - name: Update Comment 119 | uses: peter-evans/create-or-update-comment@v3 120 | if: success() && github.event.number && steps.fc.outputs.comment-id != 0 121 | with: 122 | issue-number: ${{ github.event.number }} 123 | body: ${{ steps.get-comment-body.outputs.body }} 124 | comment-id: ${{ steps.fc.outputs.comment-id }} 125 | edit-mode: replace 126 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: null 8 | workflow_dispatch: null 9 | jobs: 10 | test: 11 | env: 12 | SKIP_ENV_VALIDATION: true 13 | SKIP_BUILD_PRODUCT_REDIRECTS: 1 14 | GA_MEASUREMENT_ID: "${{ secrets.GA_MEASUREMENT_ID }}" 15 | GA_PROPERTY_ID: "${{ secrets.GA_PROPERTY_ID }}" 16 | GA_BASE64_SERVICE_ACCOUNT: "${{ secrets.GA_BASE64_SERVICE_ACCOUNT }}" 17 | NEXT_PUBLIC_HYGRAPH_CONTENT_API_URL: "${{ secrets.NEXT_PUBLIC_HYGRAPH_CONTENT_API_URL }}" 18 | NEXT_PUBLIC_ALGOLIA_API_ID: "${{ secrets.NEXT_PUBLIC_ALGOLIA_API_ID }}" 19 | NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY: "${{ secrets.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY }}" 20 | timeout-minutes: 60 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - run: echo "node_version=$(cat .github/nodejs.version)" >> $GITHUB_ENV 25 | - name: "use node ${{ env.node_version }}" 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: "${{ env.node_version }}" 29 | - name: Install dependencies 30 | run: yarn install --frozen-lockfile 31 | - name: Install Playwright Browsers 32 | run: yarn playwright install --with-deps 33 | - name: Run codegen 34 | run: yarn run codegen 35 | - name: Run Playwright tests 36 | run: yarn playwright test 37 | - uses: actions/upload-artifact@v3 38 | if: always() 39 | with: 40 | name: playwright-report 41 | path: playwright-report/ 42 | retention-days: 30 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | 4 | # codegen 5 | src/gql/fragment-masking.ts 6 | src/gql/gql.ts 7 | src/gql/graphql.ts 8 | src/gql/index.ts 9 | 10 | # generated on build 11 | /public/robots.txt 12 | /public/sitemap.xml 13 | /public/sitemap-*.xml 14 | 15 | 16 | # credentials 17 | 18 | google-credentials.json 19 | 20 | # Yarn 3 files 21 | .pnp.* 22 | .yarn/* 23 | !.yarn/patches 24 | !.yarn/plugins 25 | !.yarn/releases 26 | !.yarn/sdks 27 | !.yarn/versions 28 | 29 | # dependencies 30 | node_modules 31 | .pnp 32 | .pnp.js 33 | .yarn 34 | 35 | # testing 36 | coverage 37 | 38 | # next.js 39 | .next/ 40 | out/ 41 | build 42 | 43 | # misc 44 | .DS_Store 45 | *.pem 46 | 47 | # debug 48 | npm-debug.log* 49 | yarn-debug.log* 50 | yarn-error.log* 51 | .pnpm-debug.log* 52 | 53 | # local env files 54 | .env 55 | .env.local 56 | .env.development.local 57 | .env.test.local 58 | .env.production.local 59 | /test-results/ 60 | /playwright-report/ 61 | /playwright/.cache/ 62 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/qoomon/git-conventional-commits 3 | rev: v2.6.3 4 | hooks: 5 | - id: conventional-commits 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "prettier-plugin-tailwindcss" 4 | ], 5 | "trailingComma": "es5", 6 | "tabWidth": 2, 7 | "printWidth": 120, 8 | "semi": false 9 | } -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | branches: ["main"], 3 | "plugins": 4 | [ 5 | ["@semantic-release/npm", { "npmPublish": false }], 6 | "@semantic-release/release-notes-generator", 7 | "@semantic-release/github", 8 | "@semantic-release/commit-analyzer", 9 | "@semantic-release/git", 10 | "@semantic-release/changelog", 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/nextjs" 2 | const config: StorybookConfig = { 3 | stories: ["../src/components/**/*.stories.mdx", "../src/components/**/*.stories.@(js|jsx|ts|tsx)"], 4 | addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"], 5 | framework: { 6 | name: "@storybook/nextjs", 7 | options: {}, 8 | }, 9 | docs: { 10 | autodocs: "tag", 11 | }, 12 | typescript: { 13 | check: false, 14 | checkOptions: {}, 15 | reactDocgen: false, 16 | reactDocgenTypescriptOptions: { 17 | shouldExtractLiteralValuesFromEnum: true, 18 | propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true), 19 | }, 20 | }, 21 | } 22 | export default config 23 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react" 2 | import "../src/styles/tailwind.css" 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | actions: { argTypesRegex: "^on[A-Z].*" }, 7 | 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | }, 15 | } 16 | 17 | export default preview 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.experimental.classRegex": [ 3 | ["cva(?:<[^>]*>)?(([^)]*))", "[\"'`]([^\"'`]*).*?[\"'`]", "(?:twMerge|twJoin)\\(([^\\);]*)[\\);]"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Blazity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next news 2 | 3 | [![Project intro image](./project-screenshot.png)](https://hygraph-next-enterprise.vercel.app/en) 4 | 5 | Welcome to **Next.js - Hygraph news starter**, an open-source plug and play template and starter for you media apps or blogs! It's build on top of [next-enterprise](https://github.com/Blazity/next-enterprise) so it comes packed with functionalities designed to assist you in creating an app that is not only high-performing and maintainable but also enjoyable to use. 6 | 7 |
8 | 9 | Blazity Discord Banner 10 | 11 |
12 | 13 | ## Table of Contents 14 | 15 | - [Next.js Enterprise Boilerplate](#nextjs-enterprise-boilerplate) 16 | - [Table of Contents](#table-of-contents) 17 | - [More resources](#more-resources) 18 | - [🎯 Getting Started](#-getting-started) 19 | - [🚀 Deployment](#-deployment) 20 | - [📃 Scripts Overview](#-scripts-overview) 21 | - [🤝 Contribution](#-contribution) 22 | - [💌 Support](#support) 23 | - [📜 License](#-license) 24 | - [Contributors](#contributors) 25 | 26 | ## More resources 27 | 28 | You can find more in depth dive into project's underlying architecture in the [next-enteprise](https://github.com/Blazity/next-enterprise) repository. 29 | 30 | ## 🎯 Getting Started 31 | 32 | To get started with this boilerplate, follow these steps: 33 | 34 | 1. Fork & clone repository: 35 | 36 | ```bash 37 | ## Don't forget to ⭐ star and fork it first :) 38 | git clone https://github.com/ 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 153 | 154 | 155 |
Marcin Szczepaniak
Marcin Szczepaniak

💻
Max
Max

💻
Bart Stefanski
Bart Stefanski

💻
Jakub Jabłoński
Jakub Jabłoński

🚇
149 | 150 | Add your contributions 151 | 152 |
156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from "@graphql-codegen/cli" 2 | 3 | const config: CodegenConfig = { 4 | schema: process.env.NEXT_PUBLIC_HYGRAPH_CONTENT_API_URL, 5 | documents: ["./src/**/*.ts"], 6 | ignoreNoDocuments: true, // for better experience with the watcher 7 | generates: { 8 | "./src/gql/": { 9 | preset: "client", 10 | plugins: [], 11 | }, 12 | }, 13 | } 14 | 15 | export default config 16 | -------------------------------------------------------------------------------- /git-conventional-commits.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | convention: 3 | commitTypes: 4 | - feat 5 | - fix 6 | - perf 7 | - refactor 8 | - style 9 | - test 10 | - build 11 | - ops 12 | - docs 13 | - chore 14 | - merge 15 | - revert 16 | commitScopes: [] 17 | releaseTagGlobPattern: v[0-9]*.[0-9]*.[0-9]* 18 | changelog: 19 | commitTypes: 20 | - feat 21 | - fix 22 | - perf 23 | - merge 24 | includeInvalidCommits: true 25 | commitIgnoreRegexPattern: "^WIP " 26 | headlines: 27 | feat: Features 28 | fix: Bug Fixes 29 | perf: Performance Improvements 30 | merge: Merges 31 | breakingChange: BREAKING CHANGES 32 | 33 | ## GitHub 34 | # commitUrl: https://github.com/ACCOUNT/REPOSITORY/commit/%commit% 35 | # commitRangeUrl: https://github.com/ACCOUNT/REPOSITORY/compare/%from%...%to%?diff=split 36 | 37 | ## GitHub Issues 38 | # issueRegexPattern: "#[0-9]+" 39 | # issueUrl: https://github.com/ACCOUNT/REPOSITORY/issues/%issue% 40 | 41 | ## Jira Issues 42 | # issueRegexPattern: "[A-Z][A-Z0-9]+-[0-9]+" 43 | # issueUrl: https://WORKSPACE.atlassian.net/browse/%issue% 44 | -------------------------------------------------------------------------------- /graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | components/Button/Button.stories.tsx 15 | 16 | components/Button/Button.stories.tsx 17 | 18 | 19 | 20 | components/Button/Button.tsx 21 | 22 | components/Button/Button.tsx 23 | 24 | 25 | 26 | components/Button/Button.stories.tsx->components/Button/Button.tsx 27 | 28 | 29 | 30 | 31 | 32 | components/Tooltip/Tooltip.tsx 33 | 34 | components/Tooltip/Tooltip.tsx 35 | 36 | 37 | 38 | pages/_app.test.tsx 39 | 40 | pages/_app.test.tsx 41 | 42 | 43 | 44 | pages/_app.tsx 45 | 46 | pages/_app.tsx 47 | 48 | 49 | 50 | pages/_app.test.tsx->pages/_app.tsx 51 | 52 | 53 | 54 | 55 | 56 | styles/tailwind.css 57 | 58 | styles/tailwind.css 59 | 60 | 61 | 62 | pages/_app.tsx->styles/tailwind.css 63 | 64 | 65 | 66 | 67 | 68 | pages/api/health.ts 69 | 70 | pages/api/health.ts 71 | 72 | 73 | 74 | pages/index.tsx 75 | 76 | pages/index.tsx 77 | 78 | 79 | 80 | pages/index.tsx->components/Button/Button.tsx 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /i18n.ts: -------------------------------------------------------------------------------- 1 | // static i18n configuration 2 | -------------------------------------------------------------------------------- /instrumentation.ts: -------------------------------------------------------------------------------- 1 | import { registerOTel } from "@vercel/otel" 2 | 3 | export function register() { 4 | registerOTel("next-app") 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import nextJest from "next/jest.js" 2 | 3 | const createJestConfig = nextJest({ 4 | dir: "./", 5 | }) 6 | 7 | /** @type {import('jest').Config} */ 8 | const config = { 9 | setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"], 10 | testEnvironment: "jest-environment-jsdom", 11 | modulePathIgnorePatterns: ["/dist/"], 12 | testPathIgnorePatterns: ["/src/e2e"], 13 | } 14 | 15 | export default createJestConfig(config) 16 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | 3 | const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? process.env.VERCEL_URL ?? "localhost:3000" 4 | 5 | module.exports = { 6 | siteUrl: `https://${SITE_URL}`, 7 | generateRobotsTxt: true, 8 | exclude: ["/api/health", "/server-sitemap-index.xml"], 9 | robotsTxtOptions: { 10 | additionalSitemaps: [`https://${SITE_URL}/server-sitemap-index.xml`], 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import withBundleAnalyzer from "@next/bundle-analyzer" 2 | import withPlugins from "next-compose-plugins" 3 | import withNextIntl from "next-intl/plugin" 4 | import { env } from "./src/env.mjs" 5 | 6 | /** 7 | * @type {import('next').NextConfig} 8 | */ 9 | const config = withPlugins( 10 | [[withBundleAnalyzer({ enabled: env.ANALYZE })]], 11 | withNextIntl("./i18n.ts")({ 12 | reactStrictMode: true, 13 | experimental: { instrumentationHook: true }, 14 | rewrites() { 15 | return { 16 | beforeFiles: [ 17 | { source: "/healthz", destination: "/api/health" }, 18 | { source: "/api/healthz", destination: "/api/health" }, 19 | { source: "/health", destination: "/api/health" }, 20 | { source: "/ping", destination: "/api/health" }, 21 | ], 22 | } 23 | }, 24 | images: { 25 | remotePatterns: [ 26 | { 27 | protocol: "https", 28 | hostname: "**.graphassets.com", 29 | } 30 | ], 31 | }, 32 | webpack: (config) => { 33 | config.module.rules.push({ 34 | test: /\.svg$/i, 35 | use: ["@svgr/webpack"], 36 | }) 37 | return config 38 | }, 39 | }) 40 | ) 41 | 42 | export default config 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-enterprise", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm-run-all --parallel \"development\" \"codegen --watch\"", 7 | "development": "cross-env FORCE_COLOR=1 next dev", 8 | "build": "graphql-codegen --require dotenv/config --config codegen.ts && next build", 9 | "postbuild": "next-sitemap --config next-sitemap.config.js", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "lint:fix": "next lint --fix", 13 | "prettier": "prettier --check \"**/*.{js,jsx,ts,tsx}\"", 14 | "prettier:fix": "prettier --write \"**/*.{js,jsx,ts,tsx}\"", 15 | "analyze": "cross-env ANALYZE=true yarn build", 16 | "storybook": "cross-env FORCE_COLOR=1 storybook dev -p 6006", 17 | "test-storybook": "cross-env FORCE_COLOR=1 test-storybook", 18 | "build-storybook": "cross-env FORCE_COLOR=1 storybook build", 19 | "test": "cross-env FORCE_COLOR=1 jest --passWithNoTests", 20 | "e2e:headless": "playwright test", 21 | "e2e:ui": "playwright test --ui", 22 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 23 | "postinstall": "npx patch-package -y", 24 | "coupling-graph": "npx madge --extensions js,jsx,ts,tsx,css,md,mdx ./ --exclude '.next|tailwind.config.js|reset.d.ts|prettier.config.js|postcss.config.js|playwright.config.ts|next.config.js|next-env.d.ts|instrumentation.ts|e2e/|README.md|.storybook/|.eslintrc.js' --image graph.svg", 25 | "codegen": "graphql-codegen --require dotenv/config --config codegen.ts" 26 | }, 27 | "dependencies": { 28 | "@formatjs/intl-localematcher": "^0.4.0", 29 | "@google-analytics/data": "^4.0.1", 30 | "@google-cloud/local-auth": "^3.0.0", 31 | "@graphcms/rich-text-react-renderer": "^0.6.1", 32 | "@hygraph/utils": "^1.2.1", 33 | "@next/bundle-analyzer": "^13.3.0", 34 | "@radix-ui/react-accordion": "^1.1.1", 35 | "@radix-ui/react-checkbox": "^1.0.3", 36 | "@radix-ui/react-dialog": "^1.0.4", 37 | "@radix-ui/react-dropdown-menu": "^2.0.4", 38 | "@radix-ui/react-form": "^0.0.3", 39 | "@radix-ui/react-label": "^2.0.1", 40 | "@radix-ui/react-popover": "^1.0.5", 41 | "@radix-ui/react-radio-group": "^1.1.2", 42 | "@radix-ui/react-scroll-area": "^1.0.3", 43 | "@radix-ui/react-select": "^1.2.2", 44 | "@radix-ui/react-slider": "^1.1.1", 45 | "@radix-ui/react-switch": "^1.0.2", 46 | "@radix-ui/react-tabs": "^1.0.3", 47 | "@radix-ui/react-toggle-group": "^1.0.3", 48 | "@radix-ui/react-tooltip": "^1.0.5", 49 | "@semantic-release/changelog": "^6.0.3", 50 | "@semantic-release/commit-analyzer": "^10.0.0", 51 | "@semantic-release/git": "^10.0.1", 52 | "@semantic-release/github": "^9.0.0", 53 | "@semantic-release/npm": "^10.0.3", 54 | "@semantic-release/release-notes-generator": "^11.0.1", 55 | "@t3-oss/env-nextjs": "^0.6.0", 56 | "@tanstack/react-query": "^4.35.0", 57 | "@vercel/otel": "^0.3.0", 58 | "algoliasearch": "^4.19.1", 59 | "class-variance-authority": "^0.7.0", 60 | "cmdk": "^0.2.0", 61 | "feed": "^4.2.2", 62 | "googleapis": "^126.0.1", 63 | "graphql": "^16.8.0", 64 | "graphql-request": "^6.1.0", 65 | "lodash": "^4.17.21", 66 | "lucide-react": "^0.274.0", 67 | "negotiator": "^0.6.3", 68 | "next": "^13.5.5", 69 | "next-compose-plugins": "^2.2.1", 70 | "next-intl": "3.0.0-beta.19", 71 | "next-sitemap": "^4.2.3", 72 | "p-throttle": "^5.1.0", 73 | "react": "^18.2.0", 74 | "react-code-blocks": "^0.1.4", 75 | "react-dom": "^18.2.0", 76 | "react-instantsearch": "^7.0.2", 77 | "request": "^2.88.2", 78 | "tailwind-merge": "^1.10.0", 79 | "zod": "^3.21.4", 80 | "zustand": "^4.4.1" 81 | }, 82 | "devDependencies": { 83 | "@babel/core": "^7.0.0", 84 | "@graphql-codegen/cli": "^5.0.0", 85 | "@graphql-codegen/client-preset": "^4.1.0", 86 | "@graphql-typed-document-node/core": "^3.2.0", 87 | "@jest/globals": "^29.5.0", 88 | "@opentelemetry/api": "^1.4.1", 89 | "@parcel/watcher": "^2.3.0", 90 | "@playwright/test": "^1.39.0", 91 | "@storybook/addon-essentials": "^7.4.6", 92 | "@storybook/addon-interactions": "^7.4.6", 93 | "@storybook/addon-links": "^7.4.6", 94 | "@storybook/blocks": "^7.4.6", 95 | "@storybook/nextjs": "^7.4.6", 96 | "@storybook/react": "^7.4.6", 97 | "@storybook/test-runner": "^0.13.0", 98 | "@storybook/testing-library": "^0.1.0", 99 | "@svgr/webpack": "^8.1.0", 100 | "@testing-library/jest-dom": "^5.16.5", 101 | "@testing-library/react": "^14.0.0", 102 | "@total-typescript/ts-reset": "^0.5.0", 103 | "@types/lodash": "^4.14.197", 104 | "@types/negotiator": "^0.6.1", 105 | "@types/node": "^18.0.0", 106 | "@types/react": "18.2.21", 107 | "@types/react-dom": "^18.0.11", 108 | "@typescript-eslint/eslint-plugin": "^5.57.1", 109 | "@typescript-eslint/parser": "^5.54.1", 110 | "all-contributors-cli": "^6.24.0", 111 | "autoprefixer": "^10.4.14", 112 | "concurrently": "^8.2.1", 113 | "cross-env": "^7.0.3", 114 | "dotenv": "^16.3.1", 115 | "eslint": "8.46.0", 116 | "eslint-config-next": "13.4.19", 117 | "eslint-config-prettier": "^9.0.0", 118 | "eslint-config-react-app": "^7.0.1", 119 | "eslint-plugin-import": "^2.27.5", 120 | "eslint-plugin-react": "7.33.2", 121 | "eslint-plugin-storybook": "^0.6.11", 122 | "eslint-plugin-tailwindcss": "^3.12.1", 123 | "fetch-mock": "^9.11.0", 124 | "jest": "^29.5.0", 125 | "jest-environment-jsdom": "^29.5.0", 126 | "npm-run-all": "^4.1.5", 127 | "patch-package": "^8.0.0", 128 | "postcss": "^8.4.21", 129 | "postinstall-postinstall": "^2.1.0", 130 | "prettier": "latest", 131 | "prettier-plugin-tailwindcss": "^0.4.0", 132 | "storybook": "^7.4.6", 133 | "tailwindcss": "^3.2.7", 134 | "ts-jest": "^29.1.0", 135 | "tsc": "^2.0.4", 136 | "typescript": "5.1.6", 137 | "vitest": "^0.34.0" 138 | }, 139 | "engines": { 140 | "node": ">=18.15.0" 141 | }, 142 | "packageManager": "yarn@1.22.19" 143 | } 144 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test" 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | 13 | const port = 3000 14 | const baseURL = `http://localhost:${port}` 15 | 16 | export default defineConfig({ 17 | testDir: "./src/e2e", 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: "html", 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Base URL to use in actions like `await page.goto('/')`. */ 31 | baseURL, 32 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 33 | trace: "on-first-retry", 34 | }, 35 | 36 | /* Configure projects for major browsers */ 37 | projects: [ 38 | { 39 | name: "chromium", 40 | use: { ...devices["Desktop Chrome"] }, 41 | }, 42 | 43 | { 44 | name: "firefox", 45 | use: { ...devices["Desktop Firefox"] }, 46 | }, 47 | 48 | { 49 | name: "webkit", 50 | use: { ...devices["Desktop Safari"] }, 51 | }, 52 | 53 | /* Test against mobile viewports. */ 54 | // { 55 | // name: 'Mobile Chrome', 56 | // use: { ...devices['Pixel 5'] }, 57 | // }, 58 | // { 59 | // name: 'Mobile Safari', 60 | // use: { ...devices['iPhone 12'] }, 61 | // }, 62 | 63 | /* Test against branded browsers. */ 64 | // { 65 | // name: 'Microsoft Edge', 66 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 67 | // }, 68 | // { 69 | // name: 'Google Chrome', 70 | // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, 71 | // }, 72 | ], 73 | 74 | /* Run your local dev server before starting the tests */ 75 | webServer: { 76 | port, 77 | command: "yarn development", 78 | reuseExistingServer: !process.env.CI, 79 | }, 80 | }) 81 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /project-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blazity/next-news/6a4831c90dfefc3b6373ffc512a9c070a8309b97/project-screenshot.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blazity/next-news/6a4831c90dfefc3b6373ffc512a9c070a8309b97/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/X.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/icons/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "packageRules": [ 7 | { 8 | "enabled": false, 9 | "matchPackagePatterns": [ 10 | "*" 11 | ] 12 | } 13 | ], 14 | "vulnerabilityAlerts": { 15 | "enabled": true 16 | }, 17 | "osvVulnerabilityAlerts": true 18 | } -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | // Do not add any other lines of code to this file! 2 | import "@total-typescript/ts-reset" 3 | -------------------------------------------------------------------------------- /src/app/(sitemaps)/[lang]/article-sitemap/[page]/server-sitemap.xml/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSideSitemap } from "next-sitemap" 2 | import { env } from "@/env.mjs" 3 | import { Locale } from "@/i18n/i18n" 4 | import { listArticlesForSitemap } from "@/lib/client" 5 | 6 | const MAX_ARTICLES_PER_SITEMAP = 1000 7 | 8 | async function generateSitemapFields(locale: Locale, pageNo: number) { 9 | const skip = (pageNo - 1) * MAX_ARTICLES_PER_SITEMAP 10 | 11 | const articles = await listArticlesForSitemap({ locale, skip, first: MAX_ARTICLES_PER_SITEMAP }) 12 | 13 | const mappedArticles = articles.map((article) => ({ 14 | loc: `${env.NEXT_PUBLIC_SITE_URL}/${locale}/articles/${article.slug}`, 15 | lastmod: article.updatedAt, 16 | priority: 0.6, 17 | changefreq: "daily" as const, 18 | images: article.image?.data ? [{ loc: new URL(article.image.data.url) }] : undefined, 19 | })) 20 | 21 | return mappedArticles 22 | } 23 | 24 | export async function GET(req: Request, { params }: { params: { lang: Locale; page: string } }) { 25 | const pageNo = parseInt(params.page) 26 | const sitemapFields = await generateSitemapFields(params.lang, pageNo) 27 | 28 | const headers = { 29 | "Cache-Control": "public, max-age=86400, s-maxage=86400, stale-while-revalidate", 30 | "Content-Type": "application/xml", 31 | } 32 | 33 | return getServerSideSitemap(sitemapFields, headers) 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(sitemaps)/[lang]/sitemap.xml/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSideSitemap } from "next-sitemap" 2 | import { env } from "@/env.mjs" 3 | import { Locale } from "@/i18n/i18n" 4 | import { listPagesForSitemap } from "@/lib/client" 5 | 6 | async function generateSitemapFields(locale: Locale) { 7 | const pages = await listPagesForSitemap(locale) 8 | 9 | const mappedPages = pages.map((page) => ({ 10 | loc: `${env.NEXT_PUBLIC_SITE_URL}/${locale}/${page.slug}`, 11 | lastModified: null, 12 | priority: 0.8, 13 | changefreq: "monthly" as const, 14 | })) 15 | 16 | return mappedPages 17 | } 18 | 19 | export async function GET(request: Request, { params }: { params: { lang: Locale } }) { 20 | const sitemapFields = await generateSitemapFields(params.lang) 21 | 22 | const headers = { 23 | "Cache-Control": "public, max-age=86400, s-maxage=86400, stale-while-revalidate", 24 | "Content-Type": "application/xml", 25 | } 26 | 27 | return getServerSideSitemap(sitemapFields, headers) 28 | } 29 | -------------------------------------------------------------------------------- /src/app/(sitemaps)/server-sitemap-index.xml/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSideSitemapIndex } from "next-sitemap" 2 | import { env } from "@/env.mjs" 3 | import { i18n } from "@/i18n/i18n" 4 | import { getArticlesQuantity } from "@/lib/client" 5 | 6 | const URLS_PER_SITEMAP = 1000 7 | 8 | export async function GET() { 9 | const locales = i18n.locales 10 | const pagesSitemaps = locales.map((locale) => `${env.NEXT_PUBLIC_SITE_URL}/${locale}/sitemap.xml`) 11 | 12 | const articlesSitemapsPromises = locales.map(async (locale) => { 13 | const allArticlesCount = await getArticlesQuantity(locale) 14 | 15 | const amountOfSitemapFiles = Math.ceil(allArticlesCount / URLS_PER_SITEMAP) 16 | 17 | return Array(amountOfSitemapFiles) 18 | .fill("") 19 | .map((_, index) => `${env.NEXT_PUBLIC_SITE_URL}/${locale}/article-sitemap/${index + 1}/server-sitemap.xml`) 20 | }) 21 | 22 | const articlesSitemaps = (await Promise.all(articlesSitemapsPromises)).flat() 23 | 24 | return getServerSideSitemapIndex([...pagesSitemaps, ...articlesSitemaps]) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/GoogleAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script" 2 | import { env } from "@/env.mjs" 3 | 4 | export function GoogleAnalytics() { 5 | return ( 6 | <> 7 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/app/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query" 4 | import { NextIntlClientProvider } from "next-intl" 5 | import { ReactNode, useState } from "react" 6 | import { TooltipProvider } from "@/components/ui/Tooltip/Tooltip" 7 | import { Locale } from "@/i18n/i18n" 8 | import { GlobalTranslations } from "@/i18n/setTranslations" 9 | import { TranslationsProvider } from "@/i18n/useTranslations" 10 | 11 | export default function Providers({ 12 | children, 13 | translations, 14 | locale, 15 | }: { 16 | children: ReactNode 17 | translations: GlobalTranslations 18 | locale: Locale 19 | }) { 20 | const [queryClient] = useState( 21 | () => 22 | new QueryClient({ 23 | defaultOptions: { 24 | queries: { 25 | refetchOnMount: false, 26 | refetchOnWindowFocus: false, 27 | }, 28 | }, 29 | }) 30 | ) 31 | return ( 32 | 33 | 34 | 35 | {children} 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/app/[lang]/[...rest]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | 3 | export default function CatchAllPage() { 4 | notFound() 5 | } 6 | -------------------------------------------------------------------------------- /src/app/[lang]/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { notFound } from "next/navigation" 3 | import { unstable_setRequestLocale } from "next-intl/server" 4 | import { RichText } from "@/components/RichText/RichText" 5 | import { hygraphLocaleToStandardNotation, i18n, Locale } from "@/i18n/i18n" 6 | import { getPageBySlug, getPageMetadataBySlug, listPagesForSitemap } from "@/lib/client" 7 | import { getMatadataObj } from "@/utils/getMetadataObj" 8 | 9 | type CustomPageProps = { 10 | params: { slug: string; lang: Locale } 11 | } 12 | 13 | export async function generateStaticParams() { 14 | const pages = await Promise.all(i18n.locales.map((locale) => listPagesForSitemap(locale))) 15 | const flatPages = pages.flatMap((pages) => pages) 16 | return flatPages.map(({ slug, locale }) => ({ 17 | lang: hygraphLocaleToStandardNotation(locale), 18 | slug, 19 | })) 20 | } 21 | 22 | export async function generateMetadata({ params: { slug, lang } }: CustomPageProps): Promise { 23 | const metaData = await getPageMetadataBySlug({ locale: lang, slug }) 24 | if (!metaData) return null 25 | 26 | const { seoComponent } = metaData 27 | 28 | return getMatadataObj({ title: seoComponent?.title, description: seoComponent?.description?.text }) 29 | } 30 | 31 | export default async function Web({ params: { slug, lang } }: CustomPageProps) { 32 | unstable_setRequestLocale(lang) 33 | const page = await getPageBySlug({ locale: lang, slug }) 34 | 35 | if (!page) notFound() 36 | return ( 37 |
38 |

{page.title}

39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/app/[lang]/article/[slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/Skeleton/Skeleton" 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/[lang]/article/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { HeroArticleCard } from "@/components/ArticleCard/HeroArticleCard" 2 | import { RecommendedArticles } from "@/components/RecommendedArticles/RecommendedArticles" 3 | import { RichText } from "@/components/RichText/RichText" 4 | import { ShareOnSocial } from "@/components/ShareOnSocial/ShareOnSocial" 5 | import { env } from "@/env.mjs" 6 | import { Locale } from "@/i18n/i18n" 7 | import { getArticleBySlug, getArticleMetadataBySlug } from "@/lib/client" 8 | import { getMatadataObj } from "@/utils/getMetadataObj" 9 | import { notFound } from "next/navigation" 10 | import { Metadata } from "next/types" 11 | 12 | type ArticlePageProps = { params: { slug: string; lang: Locale } } 13 | 14 | export async function generateMetadata({ params: { slug, lang } }: ArticlePageProps): Promise { 15 | const article = await getArticleMetadataBySlug({ locale: lang, slug }) 16 | if (!article) return null 17 | const { seoComponent, image } = article 18 | 19 | const description = seoComponent?.description?.text 20 | const title = seoComponent?.title 21 | 22 | return getMatadataObj({ description, title, image }) 23 | } 24 | 25 | export default async function Web({ params: { slug, lang } }: ArticlePageProps) { 26 | const article = await getArticleBySlug({ locale: lang, slug }) 27 | const articleUrl = `${env.NEXT_PUBLIC_SITE_URL}/article/${slug}` 28 | const initialQuiz = article?.content?.references[0] 29 | 30 | if (!article) return notFound() 31 | 32 | const { image, publishedAt, title, tags, author } = article 33 | return ( 34 | <> 35 |
36 | tag), 44 | slug, 45 | }} 46 | asLink={false} 47 | /> 48 | 49 | {article.content && ( 50 |
51 | 52 |
53 | )} 54 |
55 |
56 | 57 |
58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/app/[lang]/category/[slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/Skeleton/Skeleton" 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/[lang]/category/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next/types" 2 | import { unstable_setRequestLocale } from "next-intl/server" 3 | import { CategoryArticles } from "@/components/CategoryArticles/CategoryArticles" 4 | import { Locale } from "@/i18n/i18n" 5 | import { setTranslations } from "@/i18n/setTranslations" 6 | import { getMatadataObj } from "@/utils/getMetadataObj" 7 | 8 | type ArticlePageProps = { params: { slug: string; lang: Locale } } 9 | 10 | export async function generateMetadata({ params: { slug } }: ArticlePageProps): Promise { 11 | return getMatadataObj({ title: `Category - ${slug}` }) 12 | } 13 | 14 | export default async function Web({ params: { slug, lang } }: ArticlePageProps) { 15 | unstable_setRequestLocale(lang) 16 | await setTranslations(lang) 17 | 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /src/app/[lang]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | export default function Error({ reset }: { error: Error & { digest?: string }; reset: () => void }) { 4 | return ( 5 |
6 |

Something went wrong!

7 | 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/app/[lang]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | import { unstable_setRequestLocale } from "next-intl/server" 3 | import { Footer } from "@/components/Footer/Footer" 4 | import { Navigation } from "@/components/Navigation/Navigation" 5 | import { env } from "@/env.mjs" 6 | import { i18n, type Locale } from "@/i18n/i18n" 7 | import { setTranslations } from "@/i18n/setTranslations" 8 | import { getNavigation } from "@/lib/client" 9 | import "@/styles/tailwind.css" 10 | import { GoogleAnalytics } from "../GoogleAnalytics" 11 | import Providers from "../Providers" 12 | 13 | export async function generateMetadata({ params }: { params: { lang: Locale } }) { 14 | const locale = params.lang ?? i18n.defaultLocale 15 | return { 16 | title: "Blazity-Hygraph news starter", 17 | openGraph: { 18 | url: env.NEXT_PUBLIC_SITE_URL, 19 | images: [ 20 | { 21 | url: "https://raw.githubusercontent.com/Blazity/next-enterprise/main/project-logo.png", 22 | width: 1200, 23 | height: 630, 24 | }, 25 | ], 26 | }, 27 | twitter: { 28 | card: "summary_large_image", 29 | }, 30 | alternates: { 31 | types: { 32 | "application/rss+xml": `${env.NEXT_PUBLIC_SITE_URL}/api/${locale}`, 33 | }, 34 | }, 35 | } 36 | } 37 | 38 | export default async function Layout({ children, params }: { children: React.ReactNode; params: { lang?: Locale } }) { 39 | const locale = params.lang ?? i18n.defaultLocale 40 | const isValidLocale = i18n.locales.some((cur) => cur === locale) 41 | if (!isValidLocale) notFound() 42 | unstable_setRequestLocale(locale) 43 | const translations = await setTranslations(locale) 44 | const { navigation, footer } = await getNavigation(locale) 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 |
52 | 55 |
56 | 57 |
{children}
58 |