Restructure routes, add tests and make pathmap dynamic

pull/1331/head
Kamran Ahmed 5 years ago
parent 159741f0af
commit 796f1cdac0
  1. 84
      __tests__/path-map.spec.js
  2. 2
      components/featured-content/guides.js
  3. 2
      components/featured-content/roadmaps.js
  4. 2
      components/guide-block/index.js
  5. 2
      components/roadmap-block/index.js
  6. 2
      components/roadmap-summary/index.js
  7. 12
      data/guides.json
  8. 0
      data/guides/bash-guide.md
  9. 0
      data/guides/dns-in-one-picture.md
  10. 0
      data/guides/http-caching.md
  11. 0
      data/guides/using-react-hooks.md
  12. 6
      data/roadmaps.json
  13. 4
      lib/guide.js
  14. 13
      lib/roadmap.js
  15. 23
      next.config.js
  16. 7
      package.json
  17. 3
      pages/[fallback]/[version].js
  18. 22
      pages/[fallback]/index.js
  19. 0
      pages/[roadmap]/[version].js
  20. 4
      pages/[roadmap]/index.js
  21. 4
      pages/roadmaps.js
  22. 58
      path-map.js
  23. 1639
      yarn.lock

@ -0,0 +1,84 @@
const path = require('path');
const fs = require('fs');
const guides = require('../data/guides');
const roadmaps = require('../data/roadmaps');
const {
getPageRoutes,
getGuideRoutes,
getRoadmapRoutes
} = require("../path-map");
describe("Build scripts tests", () => {
test('it should generate valid pathmap for pages', () => {
const pageRoutes = getPageRoutes();
expect(pageRoutes).toEqual({
'/': { page: '/index' },
'/about': { page: '/about' },
'/privacy': { page: '/privacy' },
'/terms': { page: '/terms' },
'/guides': { page: '/guides/index' },
'/roadmaps': { page: '/roadmaps' },
});
});
test('it should generate valid guides pathmap', () => {
const expectedGuideRoutes = guides.reduce((acc, guide) => {
const [,, slug] = guide.url.split('/');
return {
...acc,
[guide.url]: {
page: '/guides/[guide]',
query: slug,
}
};
}, {});
// Valid path map is generated
expect(expectedGuideRoutes).toEqual(getGuideRoutes());
const pageFilePath = path.join(__dirname, '../pages/guides/[guide].js');
const foundFilePath = fs.existsSync(pageFilePath) ? pageFilePath : '';
// Given page component exists
expect(foundFilePath).toEqual(pageFilePath);
});
test('it should have markdown file for each guide', () => {
guides.forEach(guide => {
const [,, slug] = guide.url.split('/');
const expectedFile = path.join(__dirname, `../data/guides/${slug}.md`);
const foundFile = fs.existsSync(expectedFile) ? expectedFile : '';
expect(foundFile).toEqual(expectedFile);
})
});
test('it should generate valid roadmap routes', () => {
const expectedPathMap = roadmaps.reduce((roadmapAcc, roadmap) => {
// Routes for each of the versions of this roadmap
const versionRoutes = roadmap.versions.reduce((versionAcc, version) => ({
...versionAcc,
[`${roadmap.url}/${version}`]: {
page: '/[roadmap]/[version]',
query: `${roadmap.url.split('/')[1]}/${version}`,
}
}), {});
// Route for the route roadmap itself
return {
...roadmapAcc,
[roadmap.url]: {
page: '/[roadmap]/index',
query: roadmap.url.split('/')[1]
},
// Expected roadmap for versions
...versionRoutes
};
}, {});
expect(getRoadmapRoutes()).toEqual(expectedPathMap);
})
});

@ -16,7 +16,7 @@ const FeaturedGuides = () => (
{ guides { guides
.filter(({ featured }) => featured) .filter(({ featured }) => featured)
.map(guide => ( .map(guide => (
<GuideBlock guide={ guide } key={ guide.slug } /> <GuideBlock guide={ guide } key={ guide.url } />
)) } )) }
</div> </div>
</div> </div>

@ -21,7 +21,7 @@ const FeaturedRoadmaps = () => (
{ roadmaps { roadmaps
.filter(({ featured }) => featured) .filter(({ featured }) => featured)
.map(roadmap => ( .map(roadmap => (
<RoadmapBlock roadmap={ roadmap } key={ roadmap.slug } /> <RoadmapBlock roadmap={ roadmap } key={ roadmap.url } />
)) } )) }
</div> </div>
</div> </div>

@ -6,7 +6,7 @@ const GuideBlock = ({ guide }) => {
const author = findByUsername(guide.author) || {}; const author = findByUsername(guide.author) || {};
return ( return (
<div className="col-xl-4 col-lg-4 col-md-6 col-sm-12 col-12 grid-item-container"> <div className="col-xl-4 col-lg-4 col-md-6 col-sm-12 col-12 grid-item-container">
<Link href={ guide.slug } passHref> <Link href={ guide.url } passHref>
<BlockLink> <BlockLink>
<BlockTitle>{ guide.title }</BlockTitle> <BlockTitle>{ guide.title }</BlockTitle>
<BlockSubtitle>{ guide.featuredDescription || guide.description }</BlockSubtitle> <BlockSubtitle>{ guide.featuredDescription || guide.description }</BlockSubtitle>

@ -3,7 +3,7 @@ import { BlockLink, BlockSubtitle, BlockTitle } from './style';
const RoadmapBlock = ({ roadmap }) => ( const RoadmapBlock = ({ roadmap }) => (
<div className="col-xl-4 col-lg-4 col-md-6 col-sm-12 col-12 grid-item-container"> <div className="col-xl-4 col-lg-4 col-md-6 col-sm-12 col-12 grid-item-container">
<Link href={ roadmap.slug } passHref> <Link href={ roadmap.url } passHref>
<BlockLink> <BlockLink>
<BlockTitle>{ roadmap.title }</BlockTitle> <BlockTitle>{ roadmap.title }</BlockTitle>
<BlockSubtitle>{ roadmap.featuredDescription || roadmap.description }</BlockSubtitle> <BlockSubtitle>{ roadmap.featuredDescription || roadmap.description }</BlockSubtitle>

@ -24,7 +24,7 @@ const RoadmapSummary = ({ roadmap }) => (
<Description>{ roadmap.description }</Description> <Description>{ roadmap.description }</Description>
<VersionList className="border-bottom"> <VersionList className="border-bottom">
{ (roadmap.versions || []).map(versionItem => ( { (roadmap.versions || []).map(versionItem => (
<Link href={ `${roadmap.slug}/${versionItem}` } passHref key={ versionItem }> <Link href={ `${roadmap.url}/${versionItem}` } passHref key={ versionItem }>
<VersionLink className={ classNames({ <VersionLink className={ classNames({
active: isActiveRoadmap(versionItem, roadmap.version), active: isActiveRoadmap(versionItem, roadmap.version),
}) }>{ versionItem } Version</VersionLink> }) }>{ versionItem } Version</VersionLink>

@ -2,7 +2,7 @@
{ {
"title": "Design Patterns for Humans", "title": "Design Patterns for Humans",
"description": "A language agnostic, ultra-simplified explanation to design patterns", "description": "A language agnostic, ultra-simplified explanation to design patterns",
"slug": "/guides/design-patterns-for-humans", "url": "/guides/design-patterns-for-humans",
"featured": true, "featured": true,
"author": "kamranahmedse", "author": "kamranahmedse",
"createdAt": "June 12, 2019", "createdAt": "June 12, 2019",
@ -11,7 +11,7 @@
{ {
"title": "Learn Regex", "title": "Learn Regex",
"description": "An easy to understand guide on regular expressions with real world examples", "description": "An easy to understand guide on regular expressions with real world examples",
"slug": "/guides/learn-regex", "url": "/guides/learn-regex",
"featured": true, "featured": true,
"author": "ziishaned", "author": "ziishaned",
"createdDate": "June 19, 2019", "createdDate": "June 19, 2019",
@ -20,7 +20,7 @@
{ {
"title": "Bash Guide", "title": "Bash Guide",
"description": "Easy to understand guide for bash with real world usage examples.", "description": "Easy to understand guide for bash with real world usage examples.",
"slug": "/guides/bash-guide", "url": "/guides/bash-guide",
"featured": true, "featured": true,
"author": "idnan", "author": "idnan",
"createdAt": "May 18, 2018", "createdAt": "May 18, 2018",
@ -29,7 +29,7 @@
{ {
"title": "DNS in One Picture", "title": "DNS in One Picture",
"description": "Quick illustrative guide on how a website is found on the internet.", "description": "Quick illustrative guide on how a website is found on the internet.",
"slug": "/guides/dns-in-one-picture", "url": "/guides/dns-in-one-picture",
"featured": true, "featured": true,
"author": "kamranahmedse", "author": "kamranahmedse",
"createdAt": "May 11, 2018", "createdAt": "May 11, 2018",
@ -38,7 +38,7 @@
{ {
"title": "Using React Hooks", "title": "Using React Hooks",
"description": "Start using React hooks in your react applications today with this guide.", "description": "Start using React hooks in your react applications today with this guide.",
"slug": "/guides/using-react-hooks", "url": "/guides/using-react-hooks",
"featured": true, "featured": true,
"author": "kamranahmedse", "author": "kamranahmedse",
"createdAt": "October 22, 2019", "createdAt": "October 22, 2019",
@ -47,7 +47,7 @@
{ {
"title": "HTTP Caching", "title": "HTTP Caching",
"description": "Everything you need to know about web caching", "description": "Everything you need to know about web caching",
"slug": "/guides/http-caching", "url": "/guides/http-caching",
"featured": true, "featured": true,
"author": "kamranahmedse", "author": "kamranahmedse",
"updatedAt": "November 01, 2019", "updatedAt": "November 01, 2019",

@ -3,7 +3,7 @@
"title": "Frontend Developer", "title": "Frontend Developer",
"description": "Step by step guide to becoming a modern frontend developer", "description": "Step by step guide to becoming a modern frontend developer",
"featuredDescription": "Step by step guide to becoming a modern frontend developer in 2019", "featuredDescription": "Step by step guide to becoming a modern frontend developer in 2019",
"slug": "/roadmaps/frontend", "url": "/frontend",
"picture": "/static/roadmaps/{version}/frontend.png", "picture": "/static/roadmaps/{version}/frontend.png",
"featured": true, "featured": true,
"versions": [ "versions": [
@ -16,7 +16,7 @@
"title": "Backend Developer", "title": "Backend Developer",
"description": "Step by step guide to becoming a modern backend developer", "description": "Step by step guide to becoming a modern backend developer",
"featuredDescription": "Step by step guide to becoming a modern backend developer in 2019", "featuredDescription": "Step by step guide to becoming a modern backend developer in 2019",
"slug": "/roadmaps/backend", "url": "/backend",
"picture": "/static/roadmaps/{version}/backend.png", "picture": "/static/roadmaps/{version}/backend.png",
"featured": true, "featured": true,
"versions": [ "versions": [
@ -29,7 +29,7 @@
"title": "DevOps Roadmap", "title": "DevOps Roadmap",
"description": "Step by step guide for DevOps or any other Operations Role", "description": "Step by step guide for DevOps or any other Operations Role",
"featuredDescription": "Step by step guide to become an SRE or for any operations role in 2019", "featuredDescription": "Step by step guide to become an SRE or for any operations role in 2019",
"slug": "/roadmaps/devops", "url": "/devops",
"picture": "/static/roadmaps/{version}/devops.png", "picture": "/static/roadmaps/{version}/devops.png",
"featured": true, "featured": true,
"versions": [ "versions": [

@ -1,7 +1,7 @@
import guides from "data/guides"; import guides from "data/guides";
export const getRequestedGuide = req => { export const getRequestedGuide = req => {
const guide = guides.find(guide => guide.slug === req.url); const guide = guides.find(guide => guide.url === req.url);
if (!guide) { if (!guide) {
return null; return null;
} }
@ -9,7 +9,7 @@ export const getRequestedGuide = req => {
// We will use this URL format to find the relevant markdown // We will use this URL format to find the relevant markdown
// file inside the `/data` directory. For example `/guides/learn-regex` // file inside the `/data` directory. For example `/guides/learn-regex`
// has to have `/guides/learn-regex.md` file inside the `data` directory // has to have `/guides/learn-regex.md` file inside the `data` directory
const path = guide.slug.replace(/^\//, ''); const path = guide.url.replace(/^\//, '');
try { try {
return { return {

@ -3,23 +3,18 @@ import roadmaps from "data/roadmaps";
export const getRequestedRoadmap = req => { export const getRequestedRoadmap = req => {
// Considering it a new roadmap URL e.g. `/roadmaps/frontend` // Considering it a new roadmap URL e.g. `/roadmaps/frontend`
const currentUrl = req.url.replace(/\/$/, ''); const currentUrl = req.url.replace(/\/$/, '');
// Considering it a legacy URL e.g. converting `/frontend` to `roadmap.sh/roadmaps/frontend`
const legacyUrl = `/roadmaps${currentUrl}`;
// Get the roadmap version out of the URL e.g. `/roadmaps/frontend/2019` // Get the roadmap version out of the URL e.g. `/roadmaps/frontend/2019`
const [foundVersion = ''] = currentUrl.match(/(\d+|latest)$/) || ['latest']; const [foundVersion = ''] = currentUrl.match(/(\d+|latest)$/) || ['latest'];
const foundVersionRegex = new RegExp(`\/?${foundVersion}$`); const foundVersionRegex = new RegExp(`\/?${foundVersion}$`);
// Remove version from the URL because slugs in roadmaps list don't have versions // Remove version from the URL because urls in roadmaps list don't have versions
const newUrlWithoutVersion = currentUrl.replace(foundVersionRegex, ''); const urlWithoutVersion = currentUrl.replace(foundVersionRegex, '');
const legacyUrlWithoutVersion = legacyUrl.replace(foundVersionRegex, '');
const urlToSlugList = [ const urlToSlugList = [
currentUrl, currentUrl,
legacyUrl, urlWithoutVersion,
newUrlWithoutVersion,
legacyUrlWithoutVersion,
]; ];
const foundRoadmap = roadmaps.find(roadmap => urlToSlugList.includes(roadmap.slug)); const foundRoadmap = roadmaps.find(roadmap => urlToSlugList.includes(roadmap.url));
if (!foundRoadmap) { if (!foundRoadmap) {
return null; return null;
} }

@ -2,6 +2,11 @@ const path = require('path');
const withSass = require('@zeit/next-sass'); const withSass = require('@zeit/next-sass');
const withCSS = require('@zeit/next-css'); const withCSS = require('@zeit/next-css');
const rehypePrism = require('@mapbox/rehype-prism'); const rehypePrism = require('@mapbox/rehype-prism');
const {
getPageRoutes,
getGuideRoutes,
getRoadmapRoutes,
} = require("./path-map");
const withMDX = require('@next/mdx')({ const withMDX = require('@next/mdx')({
extension: /\.(md|mdx)?$/, extension: /\.(md|mdx)?$/,
@ -12,22 +17,10 @@ const withMDX = require('@next/mdx')({
const options = { const options = {
exportPathMap: () => { exportPathMap: () => {
// @todo make it dynamic for pages, authors and guides
return { return {
'/': { page: '/' }, ...getPageRoutes(),
'/about': { page: '/about' }, ...getGuideRoutes(),
'/privacy': { page: '/privacy' }, ...getRoadmapRoutes(),
'/terms': { page: '/terms' },
'/roadmaps': { page: '/roadmaps' },
'/guides': { page: '/guides' },
'/guides/design-patterns-for-humans': { page: '/guides/[guide]', query: "design-patterns-for-humans" },
'/frontend': { page: '/[fallback]', query: "frontend" },
'/backend': { page: '/[fallback]', query: "backend" },
'/devops': { page: '/[fallback]', query: "devops" },
'/roadmaps/frontend': { page: '/roadmaps/[roadmap]', query: "frontend" },
'/roadmaps/backend': { page: '/roadmaps/[roadmap]', query: "backend" },
'/roadmaps/devops': { page: '/roadmaps/[roadmap]', query: "devops" },
}; };
}, },

@ -7,7 +7,9 @@
"dev": "next", "dev": "next",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"html": "next export" "html": "next export",
"test": "jest",
"test:watch": "jest --watch"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.22", "@fortawesome/fontawesome-svg-core": "^1.2.22",
@ -35,6 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-styled-components": "^1.10.6", "babel-plugin-styled-components": "^1.10.6",
"glob": "^7.1.5" "glob": "^7.1.5",
"jest": "^24.9.0"
} }
} }

@ -1,3 +0,0 @@
import OldRoadmap from './index';
export default OldRoadmap;

@ -1,22 +0,0 @@
import Error from 'next/error';
import Roadmap from 'pages/roadmaps/[roadmap]/index';
import { serverOnlyProps } from 'lib/server';
import { getRequestedRoadmap } from 'lib/roadmap';
// Fallback page to handle the old roadmap pages implementation
const OldRoadmap = ({ roadmap }) => {
if (roadmap) {
return <Roadmap roadmap={ roadmap } />
}
return <Error statusCode={ 404 } />;
};
OldRoadmap.getInitialProps = serverOnlyProps(({ req }) => {
return {
roadmap: getRequestedRoadmap(req),
};
});
export default OldRoadmap;

@ -1,10 +1,10 @@
import Error from 'next/error'; import Error from 'next/error';
import DefaultLayout from 'layouts/default'; import DefaultLayout from 'layouts/default';
import { serverOnlyProps } from 'lib/server';
import PageHeader from 'components/page-header'; import PageHeader from 'components/page-header';
import PageFooter from 'components/page-footer'; import PageFooter from 'components/page-footer';
import { getRequestedRoadmap } from 'lib/roadmap';
import RoadmapSummary from 'components/roadmap-summary'; import RoadmapSummary from 'components/roadmap-summary';
import { serverOnlyProps } from 'lib/server';
import { getRequestedRoadmap } from 'lib/roadmap';
const Roadmap = ({ roadmap }) => { const Roadmap = ({ roadmap }) => {
if (!roadmap) { if (!roadmap) {

@ -1,7 +1,7 @@
import DefaultLayout from 'layouts/default/index'; import DefaultLayout from 'layouts/default/index';
import PageHeader from 'components/page-header/index'; import PageHeader from 'components/page-header/index';
const Roadmap = () => ( const RoadmapsList = () => (
<DefaultLayout> <DefaultLayout>
<PageHeader /> <PageHeader />
<div className="container"> <div className="container">
@ -10,4 +10,4 @@ const Roadmap = () => (
</DefaultLayout> </DefaultLayout>
); );
export default Roadmap; export default RoadmapsList;

@ -1,7 +1,9 @@
const path = require('path'); const path = require('path');
const fs = require('fs');
const glob = require('glob'); const glob = require('glob');
const guides = require('./data/guides.json');
const roadmaps = require('./data/roadmaps');
const PAGES_PATH = path.join(__dirname, 'pages'); const PAGES_PATH = path.join(__dirname, 'pages');
/** /**
@ -21,7 +23,6 @@ const getPageRoutes = () => {
}); });
const pageRoutes = {}; const pageRoutes = {};
files.forEach(file => { files.forEach(file => {
const pageName = file.replace(PAGES_PATH, '').replace('.js', ''); const pageName = file.replace(PAGES_PATH, '').replace('.js', '');
const pagePath = pageName.replace('/index', '') || '/'; const pagePath = pageName.replace('/index', '') || '/';
@ -32,4 +33,55 @@ const getPageRoutes = () => {
return pageRoutes; return pageRoutes;
}; };
console.log(getPageRoutes()); /**
* Generates routes for guide pages
* @returns {*}
*/
const getGuideRoutes = () => {
return guides.reduce((acc, guide) => {
const [, , slug] = guide.url.split('/');
return {
...acc,
[guide.url]: {
page: '/guides/[guide]',
query: slug,
}
};
}, {});
};
/**
* Generates routes for each of the roadmap and it's respective versions
* @returns {*}
*/
const getRoadmapRoutes = () => {
return roadmaps.reduce((roadmapRoutes, roadmap) => {
const [, slug] = roadmap.url.split('/');
return {
...roadmapRoutes,
// Default roadmap path i.e. `{ '/frontend': { page: '/[roadmap]/index', query: 'frontend' }`
[roadmap.url]: {
page: '/[roadmap]/index',
query: slug
},
// Route for each of the versions of this roadmap i.e.
// `{ '/frontend/2019': { page: '/[roadmap]/[version]', query: 'frontend/2019' } }`
...(roadmap.versions.reduce((versionRoutes, version) => {
return {
...versionRoutes,
[`${roadmap.url}/${version}`]: {
page: '/[roadmap]/[version]',
query: `${slug}/${version}`
}
};
}, {})),
};
}, {});
};
module.exports = {
getPageRoutes,
getGuideRoutes,
getRoadmapRoutes,
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save