diff --git a/.env.local.sample b/.env.local.sample index 194ec91..5ed0d32 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -3,17 +3,17 @@ # Contentstack is the tool we use to manage our website's content. # You need to replace 'your_stack_api_key', 'your_delivery_token', and 'your_environment_name' with the actual information. -CONTENTSTACK_API_KEY=your_stack_api_key -CONTENTSTACK_DELIVERY_TOKEN=your_delivery_token -CONTENTSTACK_ENVIRONMENT=your_environment_name +NEXT_PUBLIC_CONTENTSTACK_API_KEY=your_stack_api_key +NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN=your_delivery_token +NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT=your_environment_name # Live Preview lets us see changes before they are shown on the website. # Replace 'your_live_preview_token' with the actual information. -CONTENTSTACK_PREVIEW_HOST=rest-preview.contentstack.com -CONTENTSTACK_PREVIEW_TOKEN=your_live_preview_token -CONTENTSTACK_APP_HOST=app.contentstack.com -CONTENTSTACK_LIVE_PREVIEW=true -CONTENTSTACK_LIVE_EDIT_TAGS=false +NEXT_PUBLIC_CONTENTSTACK_PREVIEW_HOST=rest-preview.contentstack.com +NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN=your_live_preview_token +NEXT_PUBLIC_CONTENTSTACK_APP_HOST=app.contentstack.com +NEXT_PUBLIC_CONTENTSTACK_LIVE_PREVIEW=true +NEXT_PUBLIC_CONTENTSTACK_LIVE_EDIT_TAGS=false # These are extra settings. You can remove the '#' at the start of the line and fill these if needed. # CONTENTSTACK_API_HOST= api.contentstack.io diff --git a/.github/workflows/issues-jira.yml b/.github/workflows/issues-jira.yml new file mode 100644 index 0000000..7bf0469 --- /dev/null +++ b/.github/workflows/issues-jira.yml @@ -0,0 +1,31 @@ +name: Create Jira Ticket for Github Issue + +on: + issues: + types: [opened] + +jobs: + issue-jira: + runs-on: ubuntu-latest + steps: + + - name: Login to Jira + uses: atlassian/gajira-login@master + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + + - name: Create Jira Issue + id: create_jira + uses: atlassian/gajira-create@master + with: + project: ${{ secrets.JIRA_PROJECT }} + issuetype: ${{ secrets.JIRA_ISSUE_TYPE }} + summary: Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }} + description: | + *GitHub Issue:* ${{ github.event.issue.html_url }} + + *Description:* + ${{ github.event.issue.body }} + fields: "${{ secrets.ISSUES_JIRA_FIELDS }}" \ No newline at end of file diff --git a/.github/workflows/jira.yml b/.github/workflows/jira.yml deleted file mode 100644 index 63960ea..0000000 --- a/.github/workflows/jira.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Create JIRA ISSUE -on: - pull_request: - types: [opened] -jobs: - security: - if: ${{ github.actor == 'dependabot[bot]' || github.actor == 'snyk-bot' || contains(github.event.pull_request.head.ref, 'snyk-fix-') || contains(github.event.pull_request.head.ref, 'snyk-upgrade-')}} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Login into JIRA - uses: atlassian/gajira-login@master - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - name: Create a JIRA Issue - id: create - uses: atlassian/gajira-create@master - with: - project: ${{ secrets.JIRA_PROJECT }} - issuetype: ${{ secrets.JIRA_ISSUE_TYPE }} - summary: | - ${{ github.event.pull_request.title }} - description: | - PR: ${{ github.event.pull_request.html_url }} - - fields: "${{ secrets.JIRA_FIELDS }}" - - name: Transition issue - uses: atlassian/gajira-transition@v3 - with: - issue: ${{ steps.create.outputs.issue }} - transition: ${{ secrets.JIRA_TRANSITION }} diff --git a/.github/workflows/policy-scan.yml b/.github/workflows/policy-scan.yml new file mode 100644 index 0000000..ff25923 --- /dev/null +++ b/.github/workflows/policy-scan.yml @@ -0,0 +1,46 @@ +name: Checks the security policy and configurations +on: + pull_request: + types: [opened, synchronize, reopened] +jobs: + security-policy: + if: github.event.repository.visibility == 'public' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@master + - name: Checks for SECURITY.md policy file + run: | + if ! [[ -f "SECURITY.md" || -f ".github/SECURITY.md" ]]; then exit 1; fi + security-license: + if: github.event.repository.visibility == 'public' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@master + - name: Checks for License file + run: | + expected_license_files=("LICENSE" "LICENSE.txt" "LICENSE.md" "License.txt") + license_file_found=false + current_year=$(date +"%Y") + + for license_file in "${expected_license_files[@]}"; do + if [ -f "$license_file" ]; then + license_file_found=true + # check the license file for the current year, if not exists, exit with error + if ! grep -q "$current_year" "$license_file"; then + echo "License file $license_file does not contain the current year." + exit 2 + fi + break + fi + done + + if [ "$license_file_found" = false ]; then + echo "No license file found. Please add a license file to the repository." + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/sca-scan.yml b/.github/workflows/sca-scan.yml index bf9c1eb..f09161f 100644 --- a/.github/workflows/sca-scan.yml +++ b/.github/workflows/sca-scan.yml @@ -3,7 +3,7 @@ on: pull_request: types: [opened, synchronize, reopened] jobs: - security: + security-sca: runs-on: ubuntu-latest steps: - uses: actions/checkout@master diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml new file mode 100644 index 0000000..049c02f --- /dev/null +++ b/.github/workflows/secrets-scan.yml @@ -0,0 +1,29 @@ +name: Secrets Scan +on: + pull_request: + types: [opened, synchronize, reopened] +jobs: + security-secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '2' + ref: '${{ github.event.pull_request.head.ref }}' + - run: | + git reset --soft HEAD~1 + - name: Install Talisman + run: | + # Download Talisman + wget https://github.com/thoughtworks/talisman/releases/download/v1.37.0/talisman_linux_amd64 -O talisman + + # Checksum verification + checksum=$(sha256sum ./talisman | awk '{print $1}') + if [ "$checksum" != "8e0ae8bb7b160bf10c4fa1448beb04a32a35e63505b3dddff74a092bccaaa7e4" ]; then exit 1; fi + + # Make it executable + chmod +x talisman + - name: Run talisman + run: | + # Run Talisman with the pre-commit hook + ./talisman --githook pre-commit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 737d872..ec1856b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ yarn-error.log* .pnpm-debug.log* # local env files +.env .env*.local # vercel diff --git a/.npmrc b/.npmrc index 235a549..e69de29 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ -timeout=60000 \ No newline at end of file diff --git a/.talismanrc b/.talismanrc new file mode 100644 index 0000000..345ca2f --- /dev/null +++ b/.talismanrc @@ -0,0 +1,5 @@ +fileignoreconfig: +- filename: .github/workflows/secrets-scan.yml + ignore_detectors: + - filecontent +version: "1.0" \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 0773923..1be7e0d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @contentstack/security-admin \ No newline at end of file +* @contentstack/security-admin diff --git a/LICENSE b/LICENSE index ffb4ad0..25403cd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Contentstack +Copyright (c) 2025 Contentstack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/[page]/page.tsx b/app/[page]/page.tsx new file mode 100644 index 0000000..503be07 --- /dev/null +++ b/app/[page]/page.tsx @@ -0,0 +1,91 @@ +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { getPageRes } from '../../helper'; +import { initializeLivePreview } from '@/helper/live-preview'; +import RenderComponents from '@/components/render-components'; +import { DevToolsClient } from '@/components/devToolClient'; + +interface PageProps { + params: Promise<{ + page: string; + }>; + searchParams: Promise<{ + [key: string]: string | string[] | undefined; + }>; +} + +export async function generateMetadata({ params }: PageProps): Promise { + try { + const resolvedParams = await params; + const pageParam = resolvedParams.page; + const entryUrl = pageParam.includes('/') ? pageParam : `/${pageParam}`; + const page = await getPageRes(entryUrl); + + if (page?.seo && page.seo.enable_search_indexing) { + const seoData: Record = {}; + + for (const key in page.seo) { + if (key !== 'enable_search_indexing') { + const metaKey = key.includes('meta_') ? key.split('meta_')[1] : key; + const value = (page.seo as Record)[key]; + if (typeof value === 'string') { + seoData[metaKey] = value; + } else if (value !== null && value !== undefined) { + seoData[metaKey] = String(value); + } + } + } + + return { + title: seoData.title || `${pageParam} - Contentstack-Nextjs-Starter-App`, + description: seoData.description, + ...seoData + }; + } + + return { + title: `${pageParam} - Contentstack-Nextjs-Starter-App` + }; + } catch (error) { + console.error('Error generating metadata:', error); + return { + title: 'Contentstack-Nextjs-Starter-App' + }; + } +} + +export default async function DynamicPage({ params, searchParams }: PageProps) { + try { + const [resolvedParams, resolvedSearchParams] = await Promise.all([ + params, + searchParams + ]); + + const filteredSearchParams = Object.fromEntries( + Object.entries(resolvedSearchParams).filter(([_, value]) => value !== undefined) + ) as Record; + + await initializeLivePreview(filteredSearchParams); + const pageParam = resolvedParams.page; + const entryUrl = pageParam.includes('/') ? pageParam : `/${pageParam}`; + const page = await getPageRes(entryUrl); + + if (!page) { + notFound(); + } + + return ( + + + + ); + } catch (error) { + console.error('Error in dynamic page:', error); + notFound(); + } +} \ No newline at end of file diff --git a/app/app-layout.tsx b/app/app-layout.tsx new file mode 100644 index 0000000..7fb04c4 --- /dev/null +++ b/app/app-layout.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import Header from '../components/header'; +import Footer from '../components/footer'; +import DevTools from '../components/devtools'; +import { getHeaderRes, getFooterRes, getAllEntries } from '../helper'; +import { HeaderProps, FooterProps } from "../typescript/layout"; +import { Page } from '@/typescript/pages'; +import { unstable_cache } from 'next/cache'; +import { initializeLivePreview } from '@/helper/live-preview'; +import { DeveloperToolsProvider } from '@/provider/developer-tools-provider'; + +const getCachedNavigation = unstable_cache( + async (entries: Page[], header: HeaderProps, footer: FooterProps) => { + return dynamicHeadersAndFooter(entries, header, footer); + }, + ['navigation-data'], +); +const dynamicHeadersAndFooter = (entries: Page[], header: HeaderProps, footer: FooterProps): [HeaderProps, FooterProps] => { + const newHeader = { ...header }; + const newFooter = { ...footer }; + + if (entries.length > 0 && newHeader.navigation_menu) { + entries.forEach((entry) => { + if (!entry.title || !entry.url) return; + + const hFound = newHeader.navigation_menu.find( + (navLink) => navLink.label === entry.title + ); + + if (!hFound) { + newHeader.navigation_menu.push({ + label: entry.title, + page_reference: [ + //@ts-ignore + { title: entry.title, url: entry.url, $: entry.$ }, + ], + $: {}, + }); + } + + const fFound = newFooter.navigation.link.find( + (nlink) => nlink.title === entry.title + ); + + if (!fFound) { + newFooter.navigation.link.push({ + title: entry.title, + href: entry.url, + //@ts-ignore + $: entry.$, + }); + } + }); + } + + return [newHeader, newFooter]; +} + +export default async function AppLayout({ + children, + hasLivePreview = false, +}: { + children: React.ReactNode; + hasLivePreview?: boolean; +}) { + try { + if (hasLivePreview) { + await initializeLivePreview(); + } + const [headerData, footerData, entriesData] = await Promise.all([ + getHeaderRes(), + getFooterRes(), + getAllEntries() + ]); + + const [dynamicHeader, dynamicFooter] = await getCachedNavigation(entriesData, headerData, footerData); + + const JSON_DATA = { + header: dynamicHeader, + footer: dynamicFooter, + } + + return ( + + + {dynamicHeader &&
} +
+ {children} + +
+ {dynamicFooter &&