Repository: ProjectEvergreen/greenwood (js)

Issue:
top level pages don't correctly merge `<title>` tags
## Type of Change
- [ ] New Feature Request
- [ ] Documentation / Website
- [ ] Improvement / Suggestion
- [x] Bug
- [ ] Other (please clarify below)

## Summary
Given a structure like this
```
src/
  pages/
    about.html
    index.html
```

And this html
```html
<!DOCTYPE html>
<html lang="en" prefix="og:http://ogp.me/ns#">

  <head>
    <title>Top Level Test</title>
  </head>
  
  <body>
    <h1>Hello from the home page!!!!</h1>
  </body>

</html>
```

The output for _index.html_ `<title>` should be "Top Level Test"

## Details
There a test case with [this ready to be enabled](https://github.com/ProjectEvergreen/greenwood/blob/release/0.10.0/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js#L50) once working.

Wonder if we should make a feature where `title` in _greenwood.config.js_ could (optionally) be a formatting function?  🤔 

Pull request description:
support `<title>` tag content from top level pages and merge correctly
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #530 (kind of forgot about this one 😱 )

## Summary of Changes
1. Correctly support `<title>` content from existing top level pages / HTML pages / markdown frontmatter

## TODO
1. [x] Verify correct order of precedence - markdown > page template > app template > greenwood config
1. [x] Make sure it works for SPA
1. [x] Make an issue around custom title formatting, e.g. if a function is provided, to let the user figure it out as needed - https://github.com/ProjectEvergreen/greenwood/issues/655
    ```js
    // greenwood.config.js
    module.exports = {
      title: (subTitle = '') => {
        const title = 'Greenwood';

        return subTitle === ''
          ? title
          :  `${title} - ${subTitle}`
      }
    }
    ```

Patch:
diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js
index c8813405..79317a95 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-html.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-html.js
@@ -63,9 +63,14 @@ const getAppTemplate = (contents, userWorkspace, customImports = []) => {
   const headLinks = root.querySelectorAll('head link');
   const headMeta = root.querySelectorAll('head meta');
   const headStyles = root.querySelectorAll('head style');
+  const headTitle = root.querySelector('head title');
 
   appTemplateContents = appTemplateContents.replace(/<page-outlet><\/page-outlet>/, body);
 
+  if (headTitle) {
+    appTemplateContents = appTemplateContents.replace(/<title>(.*)<\/title>/, `<title>${headTitle.rawText}</title>`);
+  }
+
   headScripts.forEach((script) => {
     const matchNeedle = '</script>';
     const matchPos = appTemplateContents.lastIndexOf(matchNeedle);
@@ -188,7 +193,12 @@ const getUserScripts = (contents, projectDirectory) => {
 };
 
 const getMetaContent = (url, config, contents) => {
-  const title = config.title || '';
+  const existingTitleMatch = contents.match(/<title>(.*)<\/title>/);
+  const existingTitleCheck = !!(existingTitleMatch && existingTitleMatch[1] && existingTitleMatch[1] !== '');
+
+  const title = existingTitleCheck
+    ? existingTitleMatch[1]
+    : config.title;
   const metaContent = config.meta.map(item => {
     let metaHtml = '';
 
@@ -209,8 +219,12 @@ const getMetaContent = (url, config, contents) => {
       : `<meta${metaHtml}/>`;
   }).join('\n');
 
-  contents = contents.replace(/<title>(.*)<\/title>/, '');
-  contents = contents.replace('<head>', `<head><title>${title}</title>`);
+  // add an empty <title> if it's not already there
+  if (!existingTitleMatch) {
+    contents = contents.replace('<head>', '<head><title></title>');
+  }
+
+  contents = contents.replace(/<title>(.*)<\/title>/, `<title>${title}</title>`);
   contents = contents.replace('<meta-outlet></meta-outlet>', metaContent);
 
   return contents;


Test patch:
diff --git a/packages/cli/test/cases/build.config.mode-spa/build.config.mode-spa.spec.js b/packages/cli/test/cases/build.config.mode-spa/build.config.mode-spa.spec.js
index 70772e6b..78f54df8 100644
--- a/packages/cli/test/cases/build.config.mode-spa/build.config.mode-spa.spec.js
+++ b/packages/cli/test/cases/build.config.mode-spa/build.config.mode-spa.spec.js
@@ -200,6 +200,13 @@ describe('Build Greenwood With: ', function() {
         expect(jsFiles.length).to.be.equal(5);
       });
 
+      it('should have custom <title> tag in the <head>', function() {
+        const title = dom.window.document.querySelectorAll('head > title');
+
+        expect(title.length).to.be.equal(1);
+        expect(title[0].textContent).to.be.equal('My Super SPA');
+      });
+
       it('should have custom <meta> tag in the <head>', function() {
         const customMeta = Array.from(dom.window.document.querySelectorAll('head > meta'))
           .filter(meta => meta.getAttribute('property') === 'og:description');
diff --git a/packages/cli/test/cases/build.config.mode-spa/greenwood.config.js b/packages/cli/test/cases/build.config.mode-spa/greenwood.config.js
index d57e5520..e6a5066b 100644
--- a/packages/cli/test/cases/build.config.mode-spa/greenwood.config.js
+++ b/packages/cli/test/cases/build.config.mode-spa/greenwood.config.js
@@ -1,3 +1,4 @@
 module.exports = {
+  title: 'this is the wrong title',
   mode: 'spa'
 };
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.mode-spa/src/index.html b/packages/cli/test/cases/build.config.mode-spa/src/index.html
index e689f95e..ff39b20a 100644
--- a/packages/cli/test/cases/build.config.mode-spa/src/index.html
+++ b/packages/cli/test/cases/build.config.mode-spa/src/index.html
@@ -2,6 +2,7 @@
 <html lang="en" prefix="og:http://ogp.me/ns#">
 
   <head>
+    <title>My Super SPA</title>
     <meta-outlet></meta-outlet>
     <meta property="og:description" content="My custom meta content." />
     <script type="module" src="/components/footer.js"></script>
diff --git a/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js b/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js
index 2eb060b4..f2f95cc0 100644
--- a/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js
+++ b/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js
@@ -9,7 +9,9 @@
  * greenwood build
  *
  * User Config
- * Default
+ * {
+ *   title: 'a title to test correct title merging'
+ * }
  *
  * User Workspace
  * Greenwood default
@@ -49,6 +51,13 @@ describe('Build Greenwood With: ', function() {
         dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html'));
       });
     
+      it('should have custom <title> tag in the <head>', function() {
+        const title = dom.window.document.querySelectorAll('head > title');
+
+        expect(title.length).to.be.equal(1);
+        expect(title[0].textContent).to.be.equal('this is the title from the config - this is a custom markdown title');
+      });
+
       it('should correctly rendering an <h3> tag', function() {
         const heading = dom.window.document.querySelectorAll('body h3');
         
diff --git a/packages/cli/test/cases/build.default.markdown/greenwood.config.js b/packages/cli/test/cases/build.default.markdown/greenwood.config.js
new file mode 100644
index 00000000..bdbd3ad9
--- /dev/null
+++ b/packages/cli/test/cases/build.default.markdown/greenwood.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  title: 'this is the title from the config'
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.markdown/src/pages/index.md b/packages/cli/test/cases/build.default.markdown/src/pages/index.md
index 66ce731f..eb9d87ee 100644
--- a/packages/cli/test/cases/build.default.markdown/src/pages/index.md
+++ b/packages/cli/test/cases/build.default.markdown/src/pages/index.md
@@ -1,3 +1,7 @@
+---
+title: 'this is a custom markdown title'
+---
+
 ### Greenwood Markdown Test
 
 This is some markdown being rendered by Greenwood.
diff --git a/packages/cli/test/cases/build.default.workspace-templates-relative-paths/src/pages/index.html b/packages/cli/test/cases/build.default.workspace-templates-relative-paths/src/pages/index.html
index 027fe406..722c0fe9 100644
--- a/packages/cli/test/cases/build.default.workspace-templates-relative-paths/src/pages/index.html
+++ b/packages/cli/test/cases/build.default.workspace-templates-relative-paths/src/pages/index.html
@@ -2,7 +2,7 @@
 <html lang="en" prefix="og:http://ogp.me/ns#">
 
   <head>
-    <link rel="stylesheet" href="../styles/home.css"><link>
+    <link rel="stylesheet" href="../styles/home.css"></link>
   </head>
   
   <body>
diff --git a/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js b/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js
index 79acc367..6fd5d12f 100644
--- a/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js
+++ b/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js
@@ -61,7 +61,7 @@ describe('Build Greenwood With: ', function() {
         expect(customMeta[0].getAttribute('content')).to.be.equal('My custom meta content.');
       });
 
-      xit('should have the correct <title> for the home page', function() {
+      it('should have the correct <title> for the home page', function() {
         const titleTags = dom.window.document.querySelectorAll('title');
 
         expect(titleTags.length).to.equal(1);
diff --git a/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js b/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js
index 77cdc8c5..8b5bad75 100644
--- a/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js
+++ b/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js
@@ -101,7 +101,7 @@ describe('Build Greenwood With: ', function() {
       });
     });
 
-    describe('Describe Page page', function() {
+    describe('Describe Pages page', function() {
       let dom;
 
       before(async function() {