├── .eslintignore ├── .gitignore ├── next-env.d.ts ├── content ├── settings │ └── general.yml ├── navigation │ ├── it │ │ ├── header.yml │ │ └── footer.yml │ └── en │ │ ├── header.yml │ │ └── footer.yml └── pages │ ├── it │ ├── about.md │ ├── contacts.md │ └── home.md │ └── en │ ├── about.md │ ├── contacts.md │ └── home.md ├── src ├── components │ ├── PageSize │ │ ├── PageSize.module.scss │ │ └── PageSize.tsx │ ├── Link │ │ ├── Link.tsx │ │ ├── PageLink.tsx │ │ ├── LocalisedLink.tsx │ │ ├── ActivableAnchor.tsx │ │ └── NavigationLink.tsx │ ├── Section │ │ ├── Section.module.scss │ │ └── Section.tsx │ ├── PageLoadingProgress │ │ ├── PageLoadingProgress.module.scss │ │ └── PageLoadingProgress.tsx │ ├── Navigation │ │ └── Navigation.tsx │ └── LanguageSwitcher │ │ └── LanguageSwitcher.tsx ├── pages │ ├── index.tsx │ ├── [locale] │ │ ├── index.ts │ │ └── [slug].tsx │ ├── admin.tsx │ ├── _document.tsx │ └── _app.tsx ├── types │ └── app.ts ├── hooks │ └── useLocale.tsx ├── layouts │ └── default │ │ ├── DefaultLayout.module.scss │ │ ├── Header │ │ ├── Logo │ │ │ ├── Logo.module.scss │ │ │ └── Logo.tsx │ │ ├── Hamburger │ │ │ ├── Hamburger.tsx │ │ │ └── Hamburger.module.scss │ │ ├── Header.tsx │ │ └── Header.module.scss │ │ ├── index.tsx │ │ └── Footer │ │ ├── Footer.tsx │ │ └── Footer.module.scss ├── core │ ├── page-context.ts │ └── config.ts ├── models │ ├── settings.ts │ ├── pages.ts │ └── navigation.ts ├── utils │ ├── language.ts │ ├── server.ts │ ├── page.ts │ └── content.ts ├── page-components │ ├── index.ts │ ├── page-components.types.ts │ ├── DefaultPage.tsx │ └── ContactsPage.tsx └── styles │ ├── main.scss │ └── variables.scss ├── .prettierrc.json ├── @types └── globals │ └── index.d.ts ├── public ├── admin │ ├── index.html │ └── config.yml └── media │ └── logo.svg ├── next.config.js ├── resolve-tsconfig-path-to-webpack-alias.js ├── package.json ├── tsconfig.json ├── readme.md └── data └── languages.yml /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | out 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | npm-*.log 4 | dist 5 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /content/settings/general.yml: -------------------------------------------------------------------------------- 1 | brandName: Nextatic 2 | logo: media/logo.svg 3 | basePageTitle: Nextatic 4 | -------------------------------------------------------------------------------- /src/components/PageSize/PageSize.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | max-width: 1000px; 3 | margin: auto; 4 | padding: 0 20px; 5 | } 6 | -------------------------------------------------------------------------------- /content/navigation/it/header.yml: -------------------------------------------------------------------------------- 1 | links: 2 | - label: Chi siamo 3 | page: about 4 | 5 | - label: Contatti 6 | page: contacts 7 | -------------------------------------------------------------------------------- /content/pages/it/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Chi siamo 3 | created: 2020-10-02T22:37:57.864Z 4 | --- 5 | 6 | Contenuto per la pagina Chi siamo 7 | -------------------------------------------------------------------------------- /content/pages/en/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: About 3 | title: About us 4 | created: 2020-10-02T22:37:57.850Z 5 | --- 6 | 7 | About us page content 8 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Page, { getStaticProps } from './[locale]/[slug]'; 2 | 3 | export default Page; 4 | 5 | export { getStaticProps }; 6 | -------------------------------------------------------------------------------- /content/pages/en/contacts.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Contacts 3 | title: Contacts 4 | created: 2020-10-02T22:37:57.850Z 5 | --- 6 | 7 | Contacts page content 8 | -------------------------------------------------------------------------------- /content/navigation/en/header.yml: -------------------------------------------------------------------------------- 1 | title: Header 2 | links: 3 | - label: About us 4 | page: about 5 | 6 | - label: Contacts 7 | page: contacts 8 | -------------------------------------------------------------------------------- /src/components/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink, { LinkProps } from 'next/link'; 2 | 3 | export type Props = LinkProps; 4 | 5 | export default NextLink; 6 | -------------------------------------------------------------------------------- /content/navigation/it/footer.yml: -------------------------------------------------------------------------------- 1 | links: 2 | - label: Chi siamo 3 | page: about 4 | 5 | - label: info@mybrand.com 6 | href: mailto:info@mybrand.com 7 | -------------------------------------------------------------------------------- /content/pages/it/contacts.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Contacts 3 | title: Contatti 4 | created: 2020-10-02T22:37:57.850Z 5 | --- 6 | 7 | Contenuto per la pagina Contatti 8 | -------------------------------------------------------------------------------- /content/navigation/en/footer.yml: -------------------------------------------------------------------------------- 1 | title: Footer 2 | links: 3 | - label: About us 4 | page: about 5 | 6 | - label: info@mybrand.com 7 | href: mailto:info@mybrand.com 8 | -------------------------------------------------------------------------------- /src/types/app.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | 3 | export type Locale = string; 4 | 5 | export type MarkdownContent = { react: ComponentType; attributes: T }; 6 | -------------------------------------------------------------------------------- /src/hooks/useLocale.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import pageContext from 'core/page-context'; 3 | 4 | const useLocale = () => useContext(pageContext).locale; 5 | 6 | export default useLocale; 7 | -------------------------------------------------------------------------------- /src/layouts/default/DefaultLayout.module.scss: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .content { 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Section/Section.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.scss'; 2 | 3 | .section { 4 | padding: 30px 0; 5 | 6 | &.alternate { 7 | background: $color-page-alternate; 8 | } 9 | 10 | &:last-child { 11 | flex: 1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/[locale]/index.ts: -------------------------------------------------------------------------------- 1 | import Page, { getStaticProps } from './[slug]'; 2 | import { localisedStaticPathsGetter } from 'utils/page'; 3 | 4 | export default Page; 5 | 6 | export { getStaticProps }; 7 | 8 | export const getStaticPaths = localisedStaticPathsGetter([{}]); 9 | -------------------------------------------------------------------------------- /src/core/page-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { Locale } from 'types/app'; 3 | 4 | export interface PageContext { 5 | locale: Locale; 6 | } 7 | 8 | const pageContext = createContext({} as PageContext); 9 | 10 | export default pageContext; 11 | -------------------------------------------------------------------------------- /src/layouts/default/Header/Logo/Logo.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.scss'; 2 | 3 | .logo { 4 | text-decoration: none; 5 | font-size: 20px; 6 | font-weight: bold; 7 | color: $color-black; 8 | display: flex; 9 | 10 | img { 11 | height: 20px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "requirePragma": false 12 | } 13 | -------------------------------------------------------------------------------- /src/models/settings.ts: -------------------------------------------------------------------------------- 1 | import { getContent } from 'utils/content'; 2 | 3 | export interface GeneralSettings { 4 | brandName: string; 5 | logo?: string; 6 | basePageTitle: string; 7 | } 8 | 9 | export const getGeneralSettings = () => 10 | getContent('settings', 'general', null, 'yml') as GeneralSettings; 11 | -------------------------------------------------------------------------------- /src/utils/language.ts: -------------------------------------------------------------------------------- 1 | import languages from 'data/languages.yml'; 2 | import { Locale } from 'types/app'; 3 | 4 | interface LanguageInfo { 5 | name: string; 6 | nativeName: string; 7 | } 8 | 9 | export const getLanguageInfo = (locale: Locale): LanguageInfo | null => 10 | (languages as any)[locale] || null; 11 | -------------------------------------------------------------------------------- /content/pages/en/home.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Home 3 | title: Welcome 4 | created: 2020-10-02T16:40:01.429Z 5 | --- 6 | 7 | This is the home page, the default page 8 | 9 | You can edit the default page by changing the `DEFAULT_PAGE_SLUG` value in `src/core/config.ts` 10 | 11 | You can edit a page content from `/admin`, under `Pages` 12 | -------------------------------------------------------------------------------- /src/page-components/index.ts: -------------------------------------------------------------------------------- 1 | import { PageComponentsMap } from './page-components.types'; 2 | import DefaultPage from './DefaultPage'; 3 | import ContactsPage from './ContactsPage'; 4 | 5 | const PAGE_COMPONENTS_MAP: PageComponentsMap = { 6 | default: DefaultPage, 7 | contacts: ContactsPage 8 | }; 9 | 10 | export default PAGE_COMPONENTS_MAP; 11 | -------------------------------------------------------------------------------- /content/pages/it/home.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Home 3 | title: Benvenuti 4 | created: 2020-10-02T16:40:01.443Z 5 | --- 6 | 7 | Questa e' la home page, la pagina di default. 8 | 9 | Puoi modificare la pagina di default cambiando il valore di `DEFAULT_PAGE_SLUG` in `src/core/config.ts` 10 | 11 | Puoi modificare il contenuto di una pagina da `/admin`, sotto `Pages` 12 | -------------------------------------------------------------------------------- /src/pages/admin.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import { useEffect } from 'react'; 3 | import { useRouter } from 'next/router'; 4 | 5 | const Page: NextPage = () => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | router.push('/admin/index.html'); 10 | }, []); 11 | 12 | return null; 13 | }; 14 | 15 | export default Page; 16 | -------------------------------------------------------------------------------- /src/page-components/page-components.types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | import { Page } from 'models/pages'; 3 | 4 | export interface PageComponentProps { 5 | page: Page; 6 | } 7 | 8 | export type PageComponent = ComponentType; 9 | 10 | export interface PageComponentsMap { 11 | default: PageComponent; 12 | [key: string]: PageComponent; 13 | } 14 | -------------------------------------------------------------------------------- /@types/globals/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md' { 2 | export const attributes: any; 3 | export const react: React.ComponentType; 4 | } 5 | 6 | declare module '*.yaml' { 7 | export const attributes: any; 8 | export const react: React.ComponentType; 9 | } 10 | 11 | declare module '*.yml' { 12 | export const attributes: any; 13 | export const react: React.ComponentType; 14 | } 15 | -------------------------------------------------------------------------------- /src/core/config.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from 'types/app'; 2 | import get from 'lodash/get'; 3 | 4 | const cmsConfig = require('public/admin/config.yml') as any; 5 | 6 | export const DEFAULT_PAGE_SLUG = 'home'; 7 | 8 | export const DEFAULT_LOCALE: Locale = 9 | get(cmsConfig, 'i18n.default_locale') || 'en'; 10 | 11 | export const LOCALES: Locale[] = get(cmsConfig, 'i18n.locales') || ['en']; 12 | -------------------------------------------------------------------------------- /src/components/Link/PageLink.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import LocalisedLink, { Props as LinkProps } from './LocalisedLink'; 3 | 4 | type Props = Omit & { slug: string }; 5 | 6 | const PageLink: FunctionComponent = props => ( 7 | 8 | ); 9 | 10 | export default PageLink; 11 | -------------------------------------------------------------------------------- /src/utils/server.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import { DEFAULT_LOCALE } from 'core/config'; 3 | import { extname, basename } from 'path'; 4 | 5 | export const getContentList = (collection: string, localised = false) => 6 | readdirSync(`content/${collection}` + (localised ? `/${DEFAULT_LOCALE}` : '')) 7 | .filter(fname => extname(fname) === '.md') 8 | .map(fname => basename(fname, extname(fname))); 9 | -------------------------------------------------------------------------------- /src/components/PageLoadingProgress/PageLoadingProgress.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.scss'; 2 | 3 | .overlay { 4 | position: fixed; 5 | left: 0; 6 | top: 0; 7 | width: 100%; 8 | height: 100%; 9 | z-index: $z-index-page-loading-overlay; 10 | backdrop-filter: blur(1px); 11 | pointer-events: none; 12 | opacity: 0; 13 | transition: opacity 0.2s; 14 | } 15 | 16 | .show { 17 | opacity: 1; 18 | pointer-events: all; 19 | } 20 | -------------------------------------------------------------------------------- /src/models/pages.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LOCALE } from 'core/config'; 2 | import { getContent } from 'utils/content'; 3 | import { MarkdownContent } from 'types/app'; 4 | import { Locale } from 'types/app'; 5 | 6 | export type Page = MarkdownContent<{ 7 | name: string; 8 | title: string; 9 | created: string; 10 | }>; 11 | 12 | export const getPage = (slug: string, locale: Locale = DEFAULT_LOCALE) => 13 | getContent('pages', slug, locale, 'md'); 14 | -------------------------------------------------------------------------------- /src/components/PageSize/PageSize.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, FunctionComponent } from 'react'; 2 | import styles from './PageSize.module.scss'; 3 | import classNames from 'classnames'; 4 | 5 | interface Props { 6 | children?: ReactNode; 7 | className?: string; 8 | } 9 | 10 | const PageSize: FunctionComponent = ({ children, className }) => ( 11 |
{children}
12 | ); 13 | 14 | export default PageSize; 15 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | height: 100%; 7 | } 8 | 9 | #__next { 10 | height: 100%; 11 | } 12 | 13 | body { 14 | background: $color-page; 15 | font-family: $font-face-base; 16 | color: $color-text; 17 | font-smooth: always; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | font-size: 16px; 21 | } 22 | 23 | a { 24 | color: $color-link; 25 | } 26 | -------------------------------------------------------------------------------- /src/models/navigation.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LOCALE } from 'core/config'; 2 | import { getContent } from 'utils/content'; 3 | import { Locale } from 'types/app'; 4 | 5 | export type UserLink = { 6 | label: string; 7 | page?: string; 8 | href?: string; 9 | }; 10 | 11 | export interface Navigation { 12 | links: UserLink[]; 13 | } 14 | 15 | export const getNavigation = (name: string, locale: Locale = DEFAULT_LOCALE) => 16 | getContent('navigation', name, locale, 'yml'); 17 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | public render() { 5 | return ( 6 | 7 | 8 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Section/Section.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, ReactNode } from 'react'; 2 | import styles from './Section.module.scss'; 3 | import classNames from 'classnames'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | alternate?: boolean; 8 | } 9 | 10 | const Section: FunctionComponent = ({ children, alternate }) => ( 11 |
14 | {children} 15 |
16 | ); 17 | 18 | export default Section; 19 | -------------------------------------------------------------------------------- /src/layouts/default/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, ReactNode } from 'react'; 2 | import Header from './Header/Header'; 3 | import Footer from './Footer/Footer'; 4 | import styles from './DefaultLayout.module.scss'; 5 | 6 | interface Props { 7 | children?: ReactNode; 8 | } 9 | 10 | const DefaultLayout: FunctionComponent = ({ children }) => ( 11 |
12 |
13 |
{children}
14 |
15 |
16 | ); 17 | 18 | export default DefaultLayout; 19 | -------------------------------------------------------------------------------- /src/page-components/DefaultPage.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import Section from 'components/Section/Section'; 3 | import PageSize from 'components/PageSize/PageSize'; 4 | import { PageComponentProps } from './page-components.types'; 5 | 6 | const DefaultPage: FunctionComponent = ({ 7 | page: { 8 | attributes: { title }, 9 | react: Content 10 | } 11 | }) => ( 12 |
13 | 14 |

{title}

15 | 16 | 17 |
18 |
19 | ); 20 | 21 | export default DefaultPage; 22 | -------------------------------------------------------------------------------- /src/utils/page.ts: -------------------------------------------------------------------------------- 1 | import { GetStaticPaths } from 'next'; 2 | import { LOCALES } from 'core/config'; 3 | 4 | export const localisedStaticPathsGetter = ( 5 | getParamSets: Object[] | (() => Promise), 6 | fallback = false 7 | ): GetStaticPaths => async () => { 8 | const paths: Array<{ params: any }> = []; 9 | const paramSets = 10 | typeof getParamSets === 'function' ? await getParamSets() : getParamSets; 11 | 12 | for (const params of paramSets) { 13 | for (const locale of LOCALES) { 14 | paths.push({ params: { ...params, locale } }); 15 | } 16 | } 17 | 18 | return { fallback, paths }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Link/LocalisedLink.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import Link, { Props as LinkProps } from './Link'; 3 | import { Locale } from 'types/app'; 4 | import useLocale from 'hooks/useLocale'; 5 | 6 | export type Props = LinkProps & { 7 | locale?: Locale; 8 | activeClassName?: string; 9 | }; 10 | 11 | const LocalisedLink: FunctionComponent = props => { 12 | const currentLocale = useLocale(); 13 | const locale = props.locale || currentLocale; 14 | const as = `/${locale}${props.as || props.href}`; 15 | const href = `/[locale]${props.href}`; 16 | 17 | return ; 18 | }; 19 | 20 | export default LocalisedLink; 21 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | $font-face-base: Arial, sans-serif; 3 | 4 | // Base palette 5 | $color-white: #fff; 6 | $color-black: #000; 7 | $color-brand: #f42561; 8 | $color-gray-lighter: #f0f0f0; 9 | $color-gray-lightest: #f5f5f5; 10 | 11 | // Semantic colors 12 | $color-text: $color-black; 13 | $color-ruler: $color-gray-lighter; 14 | $color-page: $color-white; 15 | $color-page-alternate: $color-gray-lightest; 16 | $color-link: $color-brand; 17 | $color-hamburger: $color-black; 18 | $color-mobile-nav: rgba($color-white, 0.8); 19 | 20 | // Breakpoints 21 | $breakpoint-tablet: 900px; 22 | $breakpoint-mobile: 700px; 23 | 24 | // Layers 25 | $z-index-nav-overlay: 10; 26 | $z-index-page-loading-overlay: 20; 27 | -------------------------------------------------------------------------------- /src/layouts/default/Header/Hamburger/Hamburger.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import styles from './Hamburger.module.scss'; 3 | import classNames from 'classnames'; 4 | 5 | interface Props { 6 | isOpen?: boolean; 7 | onClick: () => void; 8 | className?: string; 9 | } 10 | 11 | const Hamburger: FunctionComponent = ({ 12 | isOpen, 13 | onClick, 14 | className 15 | }) => ( 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | export default Hamburger; 34 | -------------------------------------------------------------------------------- /src/layouts/default/Header/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import LocalisedLink from 'components/Link/LocalisedLink'; 2 | import { getGeneralSettings } from 'models/settings'; 3 | import { getPage } from 'models/pages'; 4 | import { DEFAULT_PAGE_SLUG } from 'core/config'; 5 | import styles from './Logo.module.scss'; 6 | 7 | export const Logo = () => { 8 | const { logo, brandName } = getGeneralSettings(); 9 | const defaultPage = getPage(DEFAULT_PAGE_SLUG); 10 | const { title } = defaultPage.attributes; 11 | 12 | return ( 13 | 14 | 15 | {logo ? : brandName} 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default Logo; 22 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { AppProps } from 'next/app'; 3 | import { Locale } from 'types/app'; 4 | import pageContext from 'core/page-context'; 5 | import { DEFAULT_LOCALE } from 'core/config'; 6 | import PageLoadingProgress from 'components/PageLoadingProgress/PageLoadingProgress'; 7 | import 'styles/main.scss'; 8 | 9 | const MyApp: FunctionComponent = ({ 10 | Component, 11 | pageProps, 12 | router 13 | }) => { 14 | const locale: Locale = (router.query.locale as Locale) || DEFAULT_LOCALE; 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default MyApp; 25 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const resolveTsconfigPathsToAlias = require('./resolve-tsconfig-path-to-webpack-alias'); 2 | 3 | module.exports = { 4 | webpack: cfg => { 5 | cfg.plugins = cfg.plugins || []; 6 | 7 | cfg.resolve.alias = { 8 | ...cfg.resolve.alias, 9 | ...resolveTsconfigPathsToAlias() 10 | }; 11 | 12 | cfg.plugins = [...cfg.plugins]; 13 | 14 | cfg.module.rules.push({ 15 | test: /\.md$/, 16 | loader: 'frontmatter-markdown-loader', 17 | options: { mode: ['react-component'] } 18 | }); 19 | 20 | cfg.plugins = cfg.plugins.filter( 21 | plugin => plugin.constructor.name !== 'ForkTsCheckerWebpackPlugin' 22 | ); 23 | 24 | cfg.module.rules.push({ test: /\.ya?ml$/, use: 'js-yaml-loader' }); 25 | 26 | return cfg; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { UserLink } from 'models/navigation'; 3 | import NavigationLink from 'components/Link/NavigationLink'; 4 | 5 | interface Props { 6 | links: UserLink[]; 7 | onClick?: () => void; 8 | className?: string; 9 | linkClassName?: string; 10 | activeClassName?: string; 11 | } 12 | 13 | const Navigation: FunctionComponent = ({ 14 | links, 15 | onClick, 16 | linkClassName, 17 | activeClassName 18 | }) => ( 19 | <> 20 | {links.map((link, i: number) => ( 21 | 28 | ))} 29 | 30 | ); 31 | 32 | export default Navigation; 33 | -------------------------------------------------------------------------------- /src/components/Link/ActivableAnchor.tsx: -------------------------------------------------------------------------------- 1 | import { AnchorHTMLAttributes, FunctionComponent } from 'react'; 2 | import omit from 'lodash/omit'; 3 | import { useRouter } from 'next/router'; 4 | import classNames from 'classnames'; 5 | 6 | export type Props = AnchorHTMLAttributes & { 7 | activeClassName?: string; 8 | }; 9 | 10 | const ActivableAnchor: FunctionComponent = props => { 11 | const router = useRouter(); 12 | const isActive = router.asPath === props.href; 13 | const anchorProps = omit(props, 'activeClassName'); 14 | const className = classNames( 15 | props.className, 16 | isActive && props.activeClassName 17 | ); 18 | 19 | return ( 20 | 25 | ); 26 | }; 27 | 28 | export default ActivableAnchor; 29 | -------------------------------------------------------------------------------- /resolve-tsconfig-path-to-webpack-alias.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | /** 4 | * Resolve tsconfig.json paths to Webpack aliases 5 | * @param {string} tsconfigPath - Path to tsconfig 6 | * @param {string} webpackConfigBasePath - Path from tsconfig to Webpack config to create absolute aliases 7 | * @return {object} - Webpack alias config 8 | */ 9 | function resolveTsconfigPathsToAlias({ tsconfigPath = './tsconfig.json', webpackConfigBasePath = __dirname } = {}) { 10 | const { paths } = require(tsconfigPath).compilerOptions 11 | 12 | const aliases = {} 13 | 14 | Object.keys(paths).forEach(item => { 15 | const key = item.replace('/*', '') 16 | aliases[key] = resolve(webpackConfigBasePath, paths[item][0].replace('/*', '').replace('*', '')) 17 | }) 18 | 19 | return aliases 20 | } 21 | 22 | module.exports = resolveTsconfigPathsToAlias 23 | -------------------------------------------------------------------------------- /src/layouts/default/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import PageSize from 'components/PageSize/PageSize'; 3 | import styles from './Footer.module.scss'; 4 | import { getNavigation } from 'models/navigation'; 5 | import { getGeneralSettings } from 'models/settings'; 6 | import Navigation from 'components/Navigation/Navigation'; 7 | 8 | interface Props {} 9 | 10 | const Footer: FunctionComponent = ({}) => { 11 | const { brandName } = getGeneralSettings(); 12 | const { links } = getNavigation('footer'); 13 | const copyright = `© ${brandName} ${new Date().getFullYear()}`; 14 | 15 | return ( 16 |
17 | 18 |
{copyright}
19 | 20 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /src/utils/content.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LOCALE } from 'core/config'; 2 | import { Locale } from 'types/app'; 3 | 4 | const BASE_LOCALE = DEFAULT_LOCALE; 5 | 6 | const merge = (a: any, b: any) => { 7 | if ('attributes' in a) { 8 | return { 9 | attributes: { ...a.attributes, ...(b.attributes || {}) }, 10 | react: b.react || a.react 11 | }; 12 | } 13 | 14 | return { ...a, ...b }; 15 | }; 16 | 17 | export const getContent = ( 18 | collection: string, 19 | slug: string, 20 | locale?: Locale | null, 21 | extension = 'md' 22 | ): T => { 23 | if (locale) { 24 | const base = require(`content/${collection}/${BASE_LOCALE}/${slug}.${extension}`); 25 | let translation = {}; 26 | 27 | try { 28 | translation = require(`content/${collection}/${locale}/${slug}.${extension}`); 29 | } catch (err) { 30 | console.error(`Missing ${locale} translations for ${collection}.${slug}`); 31 | } 32 | 33 | return merge(base, translation); 34 | } 35 | 36 | return require(`content/${collection}/${slug}.${extension}`); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/Link/NavigationLink.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { UserLink } from 'models/navigation'; 3 | import ActivableAnchor from 'components/Link/ActivableAnchor'; 4 | import PageLink from 'components/Link/PageLink'; 5 | 6 | interface Props { 7 | link: UserLink; 8 | onClick?: () => void; 9 | className?: string; 10 | activeClassName?: string; 11 | } 12 | 13 | const NavigationLink: FunctionComponent = ({ 14 | link, 15 | onClick, 16 | className, 17 | activeClassName 18 | }) => { 19 | const { label, page, href } = link; 20 | 21 | if (page) { 22 | return ( 23 | 24 | 29 | {label} 30 | 31 | 32 | ); 33 | } 34 | 35 | return ( 36 |
37 | {label} 38 | 39 | ); 40 | }; 41 | 42 | export default NavigationLink; 43 | -------------------------------------------------------------------------------- /src/components/PageLoadingProgress/PageLoadingProgress.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useEffect, useState, FunctionComponent } from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './PageLoadingProgress.module.scss'; 5 | 6 | const PageLoadingProgress: FunctionComponent = () => { 7 | const router = useRouter(); 8 | const [isLoading, setIsLoading] = useState(false); 9 | const onLoadStart = () => setIsLoading(true); 10 | const onLoadEnd = () => setIsLoading(false); 11 | 12 | useEffect(() => { 13 | router.events.on('routeChangeStart', onLoadStart); 14 | router.events.on('routeChangeComplete', onLoadEnd); 15 | router.events.on('routeChangeError', onLoadEnd); 16 | 17 | return () => { 18 | router.events.off('routeChangeStart', onLoadStart); 19 | router.events.off('routeChangeComplete', onLoadEnd); 20 | router.events.off('routeChangeError', onLoadEnd); 21 | }; 22 | }); 23 | 24 | return ( 25 |
26 | ); 27 | }; 28 | 29 | export default PageLoadingProgress; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextatic", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "next build", 9 | "start": "next start", 10 | "export": "npm run build && next export -o dist" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "classnames": "^2.2.6", 17 | "next": "^9.5.3", 18 | "react": "^16.13.1", 19 | "react-dom": "^16.13.1" 20 | }, 21 | "devDependencies": { 22 | "@types/classnames": "^2.2.10", 23 | "@types/lodash": "^4.14.161", 24 | "@types/node": "^14.11.2", 25 | "@types/react": "^16.9.49", 26 | "@typescript-eslint/parser": "^4.2.0", 27 | "eslint": "^7.10.0", 28 | "eslint-config-prettier": "^6.12.0", 29 | "eslint-import-resolver-webpack": "^0.12.2", 30 | "eslint-plugin-prettier": "^3.1.4", 31 | "eslint-plugin-react": "^7.21.2", 32 | "frontmatter-markdown-loader": "^3.6.0", 33 | "js-yaml-loader": "^1.2.2", 34 | "lodash": "^4.17.20", 35 | "prettier": "^2.1.2", 36 | "sass": "^1.26.11", 37 | "typescript": "^4.0.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/page-components/ContactsPage.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import Section from 'components/Section/Section'; 3 | import PageSize from 'components/PageSize/PageSize'; 4 | import { PageComponentProps } from './page-components.types'; 5 | 6 | const ContactsPage: FunctionComponent = ({ 7 | page: { 8 | attributes: { title }, 9 | react: Content 10 | } 11 | }) => ( 12 | <> 13 |
14 | 15 |

{title}

16 | 17 | 18 |
19 |
20 | 21 |
22 | 23 |

Example of a page rendered with a custom component.

24 | 25 |

26 | You can find this component in{' '} 27 | src/components/page-components/ContactsPage.tsx. 28 |

29 | 30 |

31 | In order to associate it to a Page entry from the CMS, 32 | edit the components map in{' '} 33 | src/components/page-components/index.ts. 34 |

35 |
36 |
37 | 38 | ); 39 | 40 | export default ContactsPage; 41 | -------------------------------------------------------------------------------- /src/layouts/default/Footer/Footer.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.scss'; 2 | 3 | .footer { 4 | background: $color-black; 5 | padding: 50px 0; 6 | color: $color-white; 7 | 8 | .inner { 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: space-between; 12 | align-items: center; 13 | } 14 | 15 | nav { 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: flex-end; 19 | 20 | a { 21 | color: $color-white; 22 | text-decoration: none; 23 | margin-left: 40px; 24 | transition: color 0.2s; 25 | 26 | &:first-child { 27 | margin-left: 0; 28 | } 29 | 30 | &:hover { 31 | color: $color-link; 32 | } 33 | 34 | &.active { 35 | opacity: 0.5; 36 | color: $color-white; 37 | } 38 | } 39 | } 40 | 41 | .copy { 42 | opacity: 0.5; 43 | font-size: 14px; 44 | } 45 | 46 | @media (max-width: $breakpoint-mobile) { 47 | .inner { 48 | flex-direction: column-reverse; 49 | text-align: center; 50 | } 51 | 52 | nav { 53 | flex-direction: column; 54 | text-align: center; 55 | } 56 | 57 | nav a, 58 | .copy { 59 | margin: 10px 0; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/LanguageSwitcher/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { LOCALES } from 'core/config'; 3 | import useLocale from 'hooks/useLocale'; 4 | import { Locale } from 'types/app'; 5 | import Link from 'components/Link/Link'; 6 | import { getLanguageInfo } from 'utils/language'; 7 | import { useRouter } from 'next/router'; 8 | 9 | interface Props { 10 | onClick?: () => void; 11 | } 12 | 13 | const LanguageSwitcher: FunctionComponent = ({ onClick }) => { 14 | const currentLocale = useLocale(); 15 | const otherLocales = LOCALES.filter((val: Locale) => val !== currentLocale); 16 | const router = useRouter(); 17 | const { pathname, asPath } = router; 18 | 19 | return ( 20 | <> 21 | {otherLocales.map((locale: Locale) => { 22 | const info = getLanguageInfo(locale); 23 | const displayName = (info && info.nativeName.split(',')[0]) || locale; 24 | const localisedAs = asPath.replace(`/${currentLocale}`, `/${locale}`); 25 | 26 | return ( 27 | 28 | {displayName} 29 | 30 | ); 31 | })} 32 | 33 | ); 34 | }; 35 | 36 | export default LanguageSwitcher; 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "strictNullChecks": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "baseUrl": ".", 18 | "paths": { 19 | "content/*": ["content/*"], 20 | "public/*": ["public/*"], 21 | "data/*": ["data/*"], 22 | 23 | "models/*": ["src/models/*"], 24 | "core/*": ["src/core/*"], 25 | "hooks/*": ["src/hooks/*"], 26 | "config/*": ["src/config/*"], 27 | "content/*": ["content/*"], 28 | "components/*": ["src/components/*"], 29 | "layouts/*": ["src/layouts/*"], 30 | "types/*": ["src/types/*"], 31 | "utils/*": ["src/utils/*"], 32 | "styles/*": ["src/styles/*"], 33 | "pages/*": ["src/pages/*"], 34 | "page-components/*": ["src/page-components/*"] 35 | }, 36 | "typeRoots": ["./node_modules/@types", "./@types"] 37 | }, 38 | "exclude": ["node_modules"], 39 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 40 | } 41 | -------------------------------------------------------------------------------- /src/layouts/default/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useState, FunctionComponent } from 'react'; 2 | import PageSize from 'components/PageSize/PageSize'; 3 | import classNames from 'classnames'; 4 | import styles from './Header.module.scss'; 5 | import { getNavigation } from 'models/navigation'; 6 | import useLocale from 'hooks/useLocale'; 7 | import Navigation from 'components/Navigation/Navigation'; 8 | import LanguageSwitcher from 'components/LanguageSwitcher/LanguageSwitcher'; 9 | import Hamburger from './Hamburger/Hamburger'; 10 | import Logo from './Logo/Logo'; 11 | 12 | const Header: FunctionComponent = () => { 13 | const [isOpen, setIsOpen] = useState(false); 14 | const toggle = () => setIsOpen(!isOpen); 15 | const close = () => setIsOpen(false); 16 | 17 | return ( 18 |
19 | 20 | 21 | 22 | 31 | 32 | 37 | 38 |
39 | ); 40 | }; 41 | 42 | export default Header; 43 | -------------------------------------------------------------------------------- /src/pages/[locale]/[slug].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage, GetStaticProps } from 'next'; 3 | import Head from 'next/head'; 4 | import { Locale } from 'types/app'; 5 | import { DEFAULT_PAGE_SLUG, DEFAULT_LOCALE } from 'core/config'; 6 | import DefaultLayout from 'layouts/default'; 7 | import { localisedStaticPathsGetter } from 'utils/page'; 8 | import { getGeneralSettings } from 'models/settings'; 9 | import { getPage } from 'models/pages'; 10 | import pageComponents from 'page-components/index'; 11 | 12 | interface Props { 13 | locale: Locale; 14 | slug: string; 15 | } 16 | 17 | const Page: NextPage = ({ slug, locale }) => { 18 | const page = getPage(slug, locale); 19 | const { attributes } = page; 20 | const { basePageTitle } = getGeneralSettings(); 21 | const { title } = attributes; 22 | const PageComponent = pageComponents[slug] || pageComponents.default; 23 | 24 | return ( 25 | <> 26 | 27 | 28 | {title} | {basePageTitle} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export const getStaticProps: GetStaticProps = async ({ 40 | params: { locale, slug } = {} 41 | }) => ({ 42 | props: { 43 | locale: (locale as Locale) || DEFAULT_LOCALE, 44 | slug: (slug as string) || DEFAULT_PAGE_SLUG 45 | } 46 | }); 47 | 48 | export default Page; 49 | 50 | export const getStaticPaths = localisedStaticPathsGetter(() => 51 | require('utils/server/') 52 | .getContentList('pages', true) 53 | .map((slug: string) => ({ slug })) 54 | ); 55 | -------------------------------------------------------------------------------- /src/layouts/default/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.scss'; 2 | 3 | .header { 4 | padding: 30px 0; 5 | border-bottom: 1px solid $color-ruler; 6 | 7 | .inner { 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: space-between; 11 | align-items: center; 12 | } 13 | 14 | nav { 15 | display: flex; 16 | 17 | a { 18 | color: $color-text; 19 | text-decoration: none; 20 | transition: color 0.2s; 21 | margin-left: 30px; 22 | 23 | &:first-child { 24 | margin-left: 0; 25 | } 26 | 27 | &:hover { 28 | color: $color-link; 29 | } 30 | 31 | &.active { 32 | opacity: 0.5; 33 | color: $color-text; 34 | } 35 | } 36 | } 37 | 38 | .hamburger { 39 | display: none; 40 | } 41 | 42 | @media (max-width: $breakpoint-mobile) { 43 | .hamburger { 44 | display: flex; 45 | z-index: $z-index-nav-overlay; 46 | } 47 | 48 | nav { 49 | position: fixed; 50 | left: 0; 51 | top: 0; 52 | width: 100%; 53 | height: 100%; 54 | opacity: 0; 55 | pointer-events: none; 56 | flex-direction: column; 57 | padding: 40px 0; 58 | box-sizing: border-box; 59 | backdrop-filter: blur(5px); 60 | background: rgba($color-mobile-nav, 0); 61 | transition: opacity 0.2s, background 0.5s; 62 | z-index: $z-index-nav-overlay; 63 | 64 | a { 65 | text-align: center; 66 | width: 100%; 67 | line-height: 70px; 68 | margin: 0; 69 | font-size: 18px; 70 | transform: translateY(-10px); 71 | transition: transform 0.6s; 72 | } 73 | } 74 | 75 | &.open nav { 76 | opacity: 1; 77 | pointer-events: all; 78 | background: $color-mobile-nav; 79 | 80 | a { 81 | transform: translateY(0); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /public/media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/layouts/default/Header/Hamburger/Hamburger.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.scss'; 2 | 3 | $width: 28px; 4 | $height: $width / 1.4; 5 | $thickness: 3px; 6 | 7 | .hamburger { 8 | display: flex; 9 | width: $width; 10 | height: 22.5px; 11 | position: relative; 12 | transform: rotate(0deg); 13 | transition: 0.5s ease-in-out; 14 | cursor: pointer; 15 | 16 | span { 17 | display: block; 18 | position: absolute; 19 | height: $thickness; 20 | width: 50%; 21 | background: $color-hamburger; 22 | opacity: 1; 23 | transform: rotate(0deg); 24 | transition: 0.25s ease-in-out; 25 | } 26 | 27 | span { 28 | &:nth-child(even) { 29 | border-radius: 0 $thickness $thickness 0; 30 | left: 50%; 31 | } 32 | 33 | &:nth-child(odd) { 34 | border-radius: $thickness 0 0 $thickness; 35 | left: 0; 36 | } 37 | 38 | &:nth-child(1), 39 | &:nth-child(2) { 40 | top: 0; 41 | } 42 | 43 | &:nth-child(3), 44 | &:nth-child(4) { 45 | top: $height / 2.5; 46 | } 47 | 48 | &:nth-child(5), 49 | &:nth-child(6) { 50 | top: $height / 1.25; 51 | } 52 | } 53 | 54 | &.open span { 55 | &:nth-child(1), 56 | &:nth-child(6) { 57 | transform: rotate(45deg); 58 | } 59 | 60 | &:nth-child(2), 61 | &:nth-child(5) { 62 | transform: rotate(-45deg); 63 | } 64 | 65 | &:nth-child(1) { 66 | left: $height / 9; 67 | top: $height / 6.4; 68 | } 69 | 70 | &:nth-child(2) { 71 | left: calc(50% - #{$height / 9}); 72 | top: $height / 6.4; 73 | } 74 | 75 | &:nth-child(3) { 76 | left: -50%; 77 | opacity: 0; 78 | } 79 | 80 | &:nth-child(4) { 81 | left: 100%; 82 | opacity: 0; 83 | } 84 | 85 | &:nth-child(5) { 86 | left: $height / 9; 87 | top: $height / 1.5; 88 | } 89 | 90 | &:nth-child(6) { 91 | left: calc(50% - #{$height / 9}); 92 | top: $height / 1.5; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /public/admin/config.yml: -------------------------------------------------------------------------------- 1 | backend: 2 | name: git-gateway 3 | branch: master 4 | media_folder: public/media 5 | public_folder: img 6 | i18n: 7 | structure: multiple_folders 8 | locales: [en, it] 9 | default_locale: en 10 | collections: 11 | - name: settings 12 | label: Settings 13 | files: 14 | - label: 'General' 15 | name: general 16 | file: content/settings/general.yml 17 | fields: 18 | - label: Brand name 19 | name: brandName 20 | widget: string 21 | 22 | - label: Logo 23 | name: logo 24 | widget: image 25 | required: false 26 | 27 | - label: Base page title 28 | name: basePageTitle 29 | widget: string 30 | 31 | - label: Navigation 32 | i18n: true 33 | name: navigation 34 | folder: content/navigation 35 | format: yml 36 | delete: false 37 | fields: 38 | - { label: 'Title', name: title, widget: string } 39 | - label: Links 40 | name: links 41 | i18n: true 42 | label_singular: Link 43 | widget: list 44 | fields: 45 | - label: Label 46 | name: label 47 | widget: string 48 | i18n: true 49 | 50 | - label: Page 51 | name: page 52 | widget: relation 53 | collection: pages 54 | required: false 55 | value_field: '{{slug}}' 56 | search_fields: ['name'] 57 | 58 | - label: Link 59 | name: href 60 | widget: string 61 | required: false 62 | 63 | - name: pages 64 | label: Pages 65 | label_singular: Page 66 | i18n: true 67 | folder: content/pages 68 | identifier_field: name 69 | create: true 70 | fields: 71 | - { label: 'Name', name: name, widget: string } 72 | - { label: 'Title', name: title, widget: string, i18n: true } 73 | - { label: 'Publish Date', name: created, widget: datetime } 74 | - { label: 'Body', name: body, widget: markdown, i18n: true } 75 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | alt text 2 | 3 | A fully static website generator with i18n features, Netlify serverless CMS, Next.js, SCSS and Typescript. 4 | 5 | See the [live demo](https://nexstatic.netlify.app/) 6 | 7 | ### Stack 8 | 9 | - [Netlify CMS](https://www.netlifycms.org/) 10 | - [Next.js](https://nextjs.org/) 11 | - [SCSS/SASS](https://sass-lang.com/) 12 | - [Typescript](https://www.typescriptlang.org/) 13 | 14 | ### Features 15 | 16 | - Builds to a fully static website deployable to Netlify 17 | - Built for internalisation using i18n Netlify CMS features 18 | - CMS driven routes and navigation 19 | - A good starter layout with all the essentials 20 | - Provides basic utilities to easily extend and put in use CMS entries 21 | 22 | ### Setup 23 | 24 | ``` 25 | git clone git@github.com:tancredi/nextatic.git 26 | cd nextatic 27 | npm install 28 | ``` 29 | 30 | ### Develop 31 | 32 | ``` 33 | npm run dev 34 | # The dev server runs on http://localhost:3000 by default 35 | ``` 36 | 37 | ### Build 38 | 39 | ``` 40 | npm run export 41 | # The static website will be built in the `dist` folder 42 | ``` 43 | 44 | ### Edit 45 | 46 | The admin interface will be available in `/admin` using Netlify authentication. 47 | 48 | ### Multi-language setup 49 | 50 | By default, the boilerplate is setup in English (`en` - default) and Italian (`it`) - you can change the locales and default locale under the `i18n.locales` and `i18n.default_locale` key in `public/admin/config.yml`. 51 | 52 | The changes should be reflected immediately as `src/core/config` reads the settings directly from the CMS YAML configuration. 53 | 54 | Internalisation features on Netlify CMS are still in beta, you can read about them [here](https://deploy-preview-4139--netlify-cms-www.netlify.app/docs/beta-features/#i18n-support). 55 | 56 | In a nutshell, both collections and specific fields in the config should have a `i18n: true` attribute, which will create entries in different folders. 57 | 58 | When the models pull content in the Next.js codebase using methods exported by `utils/content`, the data will extend the default language entry and extend with values from the translated entry. 59 | 60 | **Note:** you must keep the `i18n.structure` set to `multiple_folders` as this boilerplate. 61 | 62 | ### What is it good for? 63 | 64 | This is a good setup if you're looking to tackle building a static website with some CMS requirements in at least than one language, as most of the complexity is involved in the routing and merging of content from the CMS. 65 | 66 | Otherwise, there would be a lot of stripping down to do, and I would recommend starting from scratch. 67 | 68 | The main restriction is the assumption that most of the website will live under the `/[locale]/[page-slug]` route, for example `/en/my-page`. 69 | 70 | ### How does it work? 71 | 72 | As you can see from the Next.js routes in `src/pages`, most of the work is done by a single route, `[locale]/[slug].tsx`, which will load the localised content of a page entry from the CMS and render the `src/page-components/DefaultPage.tsx` component with it or any other component mapped to that slug in `src/page-components/index.ts` 73 | 74 | ### Models 75 | 76 | Modules exporting CMS entry types and sync getters can be found under `src/models`. They can be used as templates for new CMS entries, as long as the model matches definitions in the CMS configuration, found in `public/admin/config.yml` 77 | 78 | ### Netlify config 79 | 80 | Use the following configuration to deploy correctly on netlify: 81 | 82 | - **Build command:** `npm run export` 83 | - **Publish directory:** `dist` 84 | 85 | ### Contributions 86 | 87 | The project started by forking [iammary/nextjs-netlifycms-ts-starter](https://github.com/iammary/nextjs-netlifycms-ts-starter), so thanks to [@iammary](https://github.com/iammary) 88 | -------------------------------------------------------------------------------- /data/languages.yml: -------------------------------------------------------------------------------- 1 | ab: { name: 'Abkhaz', nativeName: 'аҧсуа' } 2 | aa: { name: 'Afar', nativeName: 'Afaraf' } 3 | af: { name: 'Afrikaans', nativeName: 'Afrikaans' } 4 | ak: { name: 'Akan', nativeName: 'Akan' } 5 | sq: { name: 'Albanian', nativeName: 'Shqip' } 6 | am: { name: 'Amharic', nativeName: 'አማርኛ' } 7 | ar: { name: 'Arabic', nativeName: 'العربية' } 8 | an: { name: 'Aragonese', nativeName: 'Aragonés' } 9 | hy: { name: 'Armenian', nativeName: 'Հայերեն' } 10 | as: { name: 'Assamese', nativeName: 'অসমীয়া' } 11 | av: { name: 'Avaric', nativeName: 'авар мацӀ, магӀарул мацӀ' } 12 | ae: { name: 'Avestan', nativeName: 'avesta' } 13 | ay: { name: 'Aymara', nativeName: 'aymar aru' } 14 | az: { name: 'Azerbaijani', nativeName: 'azərbaycan dili' } 15 | bm: { name: 'Bambara', nativeName: 'bamanankan' } 16 | ba: { name: 'Bashkir', nativeName: 'башҡорт теле' } 17 | eu: { name: 'Basque', nativeName: 'euskara, euskera' } 18 | be: { name: 'Belarusian', nativeName: 'Беларуская' } 19 | bn: { name: 'Bengali', nativeName: 'বাংলা' } 20 | bh: { name: 'Bihari', nativeName: 'भोजपुरी' } 21 | bi: { name: 'Bislama', nativeName: 'Bislama' } 22 | bs: { name: 'Bosnian', nativeName: 'bosanski jezik' } 23 | br: { name: 'Breton', nativeName: 'brezhoneg' } 24 | bg: { name: 'Bulgarian', nativeName: 'български език' } 25 | my: { name: 'Burmese', nativeName: 'ဗမာစာ' } 26 | ca: { name: 'Catalan; Valencian', nativeName: 'Català' } 27 | ch: { name: 'Chamorro', nativeName: 'Chamoru' } 28 | ce: { name: 'Chechen', nativeName: 'нохчийн мотт' } 29 | ny: { name: 'Chichewa; Chewa; Nyanja', nativeName: 'chiCheŵa, chinyanja' } 30 | zh: { name: 'Chinese', nativeName: '中文 (Zhōngwén), 汉语, 漢語' } 31 | cv: { name: 'Chuvash', nativeName: 'чӑваш чӗлхи' } 32 | kw: { name: 'Cornish', nativeName: 'Kernewek' } 33 | co: { name: 'Corsican', nativeName: 'corsu, lingua corsa' } 34 | cr: { name: 'Cree', nativeName: 'ᓀᐦᐃᔭᐍᐏᐣ' } 35 | hr: { name: 'Croatian', nativeName: 'hrvatski' } 36 | cs: { name: 'Czech', nativeName: 'česky, čeština' } 37 | da: { name: 'Danish', nativeName: 'dansk' } 38 | dv: { name: 'Divehi; Dhivehi; Maldivian;', nativeName: 'ދިވެހި' } 39 | nl: { name: 'Dutch', nativeName: 'Nederlands, Vlaams' } 40 | en: { name: 'English', nativeName: 'English' } 41 | eo: { name: 'Esperanto', nativeName: 'Esperanto' } 42 | et: { name: 'Estonian', nativeName: 'eesti, eesti keel' } 43 | ee: { name: 'Ewe', nativeName: 'Eʋegbe' } 44 | fo: { name: 'Faroese', nativeName: 'føroyskt' } 45 | fj: { name: 'Fijian', nativeName: 'vosa Vakaviti' } 46 | fi: { name: 'Finnish', nativeName: 'suomi, suomen kieli' } 47 | fr: { name: 'French', nativeName: 'français, langue française' } 48 | ff: 49 | { name: 'Fula; Fulah; Pulaar; Pular', nativeName: 'Fulfulde, Pulaar, Pular' } 50 | gl: { name: 'Galician', nativeName: 'Galego' } 51 | ka: { name: 'Georgian', nativeName: 'ქართული' } 52 | de: { name: 'German', nativeName: 'Deutsch' } 53 | el: { name: 'Greek, Modern', nativeName: 'Ελληνικά' } 54 | gn: { name: 'Guaraní', nativeName: 'Avañeẽ' } 55 | gu: { name: 'Gujarati', nativeName: 'ગુજરાતી' } 56 | ht: { name: 'Haitian; Haitian Creole', nativeName: 'Kreyòl ayisyen' } 57 | ha: { name: 'Hausa', nativeName: 'Hausa, هَوُسَ' } 58 | he: { name: 'Hebrew (modern)', nativeName: 'עברית' } 59 | hz: { name: 'Herero', nativeName: 'Otjiherero' } 60 | hi: { name: 'Hindi', nativeName: 'हिन्दी, हिंदी' } 61 | ho: { name: 'Hiri Motu', nativeName: 'Hiri Motu' } 62 | hu: { name: 'Hungarian', nativeName: 'Magyar' } 63 | ia: { name: 'Interlingua', nativeName: 'Interlingua' } 64 | id: { name: 'Indonesian', nativeName: 'Bahasa Indonesia' } 65 | ie: 66 | { 67 | name: Interlingue, 68 | nativeName: 'Originally called Occidental; then Interlingue after WWII', 69 | } 70 | ga: { name: 'Irish', nativeName: 'Gaeilge' } 71 | ig: { name: 'Igbo', nativeName: 'Asụsụ Igbo' } 72 | ik: { name: 'Inupiaq', nativeName: 'Iñupiaq, Iñupiatun' } 73 | io: { name: 'Ido', nativeName: 'Ido' } 74 | is: { name: 'Icelandic', nativeName: 'Íslenska' } 75 | it: { name: 'Italian', nativeName: 'Italiano' } 76 | iu: { name: 'Inuktitut', nativeName: 'ᐃᓄᒃᑎᑐᑦ' } 77 | ja: { name: 'Japanese', nativeName: '日本語 (にほんご/にっぽんご)' } 78 | jv: { name: 'Javanese', nativeName: 'basa Jawa' } 79 | kl: 80 | { 81 | name: Kalaallisut, 82 | Greenlandic, 83 | nativeName: 'kalaallisut, kalaallit oqaasii', 84 | } 85 | kn: { name: 'Kannada', nativeName: 'ಕನ್ನಡ' } 86 | kr: { name: 'Kanuri', nativeName: 'Kanuri' } 87 | ks: { name: 'Kashmiri', nativeName: 'कश्मीरी, كشميري‎' } 88 | kk: { name: 'Kazakh', nativeName: 'Қазақ тілі' } 89 | km: { name: 'Khmer', nativeName: 'ភាសាខ្មែរ' } 90 | ki: { name: 'Kikuyu, Gikuyu', nativeName: 'Gĩkũyũ' } 91 | rw: { name: 'Kinyarwanda', nativeName: 'Ikinyarwanda' } 92 | ky: { name: 'Kirghiz, Kyrgyz', nativeName: 'кыргыз тили' } 93 | kv: { name: 'Komi', nativeName: 'коми кыв' } 94 | kg: { name: 'Kongo', nativeName: 'KiKongo' } 95 | ko: { name: 'Korean', nativeName: '한국어 (韓國語), 조선말 (朝鮮語)' } 96 | ku: { name: 'Kurdish', nativeName: 'Kurdî, كوردی‎' } 97 | kj: { name: 'Kwanyama, Kuanyama', nativeName: 'Kuanyama' } 98 | la: { name: 'Latin', nativeName: 'latine, lingua latina' } 99 | lb: { name: 'Luxembourgish, Letzeburgesch', nativeName: 'Lëtzebuergesch' } 100 | lg: { name: 'Luganda', nativeName: 'Luganda' } 101 | li: { name: 'Limburgish, Limburgan, Limburger', nativeName: 'Limburgs' } 102 | ln: { name: 'Lingala', nativeName: 'Lingála' } 103 | lo: { name: 'Lao', nativeName: 'ພາສາລາວ' } 104 | lt: { name: 'Lithuanian', nativeName: 'lietuvių kalba' } 105 | lu: { name: 'Luba-Katanga', nativeName: '' } 106 | lv: { name: 'Latvian', nativeName: 'latviešu valoda' } 107 | gv: { name: 'Manx', nativeName: 'Gaelg, Gailck' } 108 | mk: { name: 'Macedonian', nativeName: 'македонски јазик' } 109 | mg: { name: 'Malagasy', nativeName: 'Malagasy fiteny' } 110 | ms: { name: 'Malay', nativeName: 'bahasa Melayu, بهاس ملايو‎' } 111 | ml: { name: 'Malayalam', nativeName: 'മലയാളം' } 112 | mt: { name: 'Maltese', nativeName: 'Malti' } 113 | mi: { name: 'Māori', nativeName: 'te reo Māori' } 114 | mr: { name: 'Marathi (Marāṭhī)', nativeName: 'मराठी' } 115 | mh: { name: 'Marshallese', nativeName: 'Kajin M̧ajeļ' } 116 | mn: { name: 'Mongolian', nativeName: 'монгол' } 117 | na: { name: 'Nauru', nativeName: 'Ekakairũ Naoero' } 118 | nv: { name: 'Navajo, Navaho', nativeName: 'Diné bizaad, Dinékʼehǰí' } 119 | nb: { name: 'Norwegian Bokmål', nativeName: 'Norsk bokmål' } 120 | nd: { name: 'North Ndebele', nativeName: 'isiNdebele' } 121 | ne: { name: 'Nepali', nativeName: 'नेपाली' } 122 | ng: { name: 'Ndonga', nativeName: 'Owambo' } 123 | nn: { name: 'Norwegian Nynorsk', nativeName: 'Norsk nynorsk' } 124 | no: { name: 'Norwegian', nativeName: 'Norsk' } 125 | ii: { name: 'Nuosu', nativeName: 'ꆈꌠ꒿ Nuosuhxop' } 126 | nr: { name: 'South Ndebele', nativeName: 'isiNdebele' } 127 | oc: { name: 'Occitan', nativeName: 'Occitan' } 128 | oj: { name: 'Ojibwe, Ojibwa', nativeName: 'ᐊᓂᔑᓈᐯᒧᐎᓐ' } 129 | cu: 130 | { 131 | name: Old Church Slavonic, 132 | Church Slavic, 133 | Church Slavonic, 134 | Old Bulgarian, 135 | Old Slavonic, 136 | nativeName: 'ѩзыкъ словѣньскъ', 137 | } 138 | om: { name: 'Oromo', nativeName: 'Afaan Oromoo' } 139 | or: { name: 'Oriya', nativeName: 'ଓଡ଼ିଆ' } 140 | os: { name: 'Ossetian, Ossetic', nativeName: 'ирон æвзаг' } 141 | pa: { name: 'Panjabi, Punjabi', nativeName: 'ਪੰਜਾਬੀ, پنجابی‎' } 142 | pi: { name: 'Pāli', nativeName: 'पाऴि' } 143 | fa: { name: 'Persian', nativeName: 'فارسی' } 144 | pl: { name: 'Polish', nativeName: 'polski' } 145 | ps: { name: 'Pashto, Pushto', nativeName: 'پښتو' } 146 | pt: { name: 'Portuguese', nativeName: 'Português' } 147 | qu: { name: 'Quechua', nativeName: 'Runa Simi, Kichwa' } 148 | rm: { name: 'Romansh', nativeName: 'rumantsch grischun' } 149 | rn: { name: 'Kirundi', nativeName: 'kiRundi' } 150 | ro: { name: 'Romanian, Moldavian, Moldovan', nativeName: 'română' } 151 | ru: { name: 'Russian', nativeName: 'русский язык' } 152 | sa: { name: 'Sanskrit (Saṁskṛta)', nativeName: 'संस्कृतम्' } 153 | sc: { name: 'Sardinian', nativeName: 'sardu' } 154 | sd: { name: 'Sindhi', nativeName: 'सिन्धी, سنڌي، سندھی‎' } 155 | se: { name: 'Northern Sami', nativeName: 'Davvisámegiella' } 156 | sm: { name: 'Samoan', nativeName: 'gagana faa Samoa' } 157 | sg: { name: 'Sango', nativeName: 'yângâ tî sängö' } 158 | sr: { name: 'Serbian', nativeName: 'српски језик' } 159 | gd: { name: 'Scottish Gaelic; Gaelic', nativeName: 'Gàidhlig' } 160 | sn: { name: 'Shona', nativeName: 'chiShona' } 161 | si: { name: 'Sinhala, Sinhalese', nativeName: 'සිංහල' } 162 | sk: { name: 'Slovak', nativeName: 'slovenčina' } 163 | sl: { name: 'Slovene', nativeName: 'slovenščina' } 164 | so: { name: 'Somali', nativeName: 'Soomaaliga, af Soomaali' } 165 | st: { name: 'Southern Sotho', nativeName: 'Sesotho' } 166 | es: { name: 'Spanish; Castilian', nativeName: 'Español, castellano' } 167 | su: { name: 'Sundanese', nativeName: 'Basa Sunda' } 168 | sw: { name: 'Swahili', nativeName: 'Kiswahili' } 169 | ss: { name: 'Swati', nativeName: 'SiSwati' } 170 | sv: { name: 'Swedish', nativeName: 'svenska' } 171 | ta: { name: 'Tamil', nativeName: 'தமிழ்' } 172 | te: { name: 'Telugu', nativeName: 'తెలుగు' } 173 | tg: { name: 'Tajik', nativeName: 'тоҷикӣ, toğikī, تاجیکی‎' } 174 | th: { name: 'Thai', nativeName: 'ไทย' } 175 | ti: { name: 'Tigrinya', nativeName: 'ትግርኛ' } 176 | bo: { name: 'Tibetan Standard, Tibetan, Central', nativeName: 'བོད་ཡིག' } 177 | tk: { name: 'Turkmen', nativeName: 'Türkmen, Түркмен' } 178 | tl: { name: 'Tagalog', nativeName: 'Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔' } 179 | tn: { name: 'Tswana', nativeName: 'Setswana' } 180 | to: { name: 'Tonga (Tonga Islands)', nativeName: 'faka Tonga' } 181 | tr: { name: 'Turkish', nativeName: 'Türkçe' } 182 | ts: { name: 'Tsonga', nativeName: 'Xitsonga' } 183 | tt: { name: 'Tatar', nativeName: 'татарча, tatarça, تاتارچا‎' } 184 | tw: { name: 'Twi', nativeName: 'Twi' } 185 | ty: { name: 'Tahitian', nativeName: 'Reo Tahiti' } 186 | ug: { name: 'Uighur, Uyghur', nativeName: 'Uyƣurqə, ئۇيغۇرچە‎' } 187 | uk: { name: 'Ukrainian', nativeName: 'українська' } 188 | ur: { name: 'Urdu', nativeName: 'اردو' } 189 | uz: { name: 'Uzbek', nativeName: 'zbek, Ўзбек, أۇزبېك‎' } 190 | ve: { name: 'Venda', nativeName: 'Tshivenḓa' } 191 | vi: { name: 'Vietnamese', nativeName: 'Tiếng Việt' } 192 | vo: { name: 'Volapük', nativeName: 'Volapük' } 193 | wa: { name: 'Walloon', nativeName: 'Walon' } 194 | cy: { name: 'Welsh', nativeName: 'Cymraeg' } 195 | wo: { name: 'Wolof', nativeName: 'Wollof' } 196 | fy: { name: 'Western Frisian', nativeName: 'Frysk' } 197 | xh: { name: 'Xhosa', nativeName: 'isiXhosa' } 198 | yi: { name: 'Yiddish', nativeName: 'ייִדיש' } 199 | yo: { name: 'Yoruba', nativeName: 'Yorùbá' } 200 | za: { name: 'Zhuang, Chuang', nativeName: 'Saɯ cueŋƅ, Saw cuengh' } 201 | --------------------------------------------------------------------------------