NEW Add ability to handle canonical URLs

This commit is contained in:
Robbie Averill 2017-08-08 13:37:46 +12:00
parent f61751b19e
commit 4aad4e728d
5 changed files with 122 additions and 29 deletions

View File

@ -568,6 +568,19 @@ class DocumentationViewer extends Controller implements PermissionProvider
return $link; return $link;
} }
/**
* Return the canonical URL from the page
*
* @return string
*/
public function getCanonicalUrl()
{
if (!$this->getPage()) {
return '';
}
return $this->getPage()->getCanonicalUrl();
}
/** /**
* Generate a list of all the pages in the documentation grouped by the * Generate a list of all the pages in the documentation grouped by the
* first letter of the page. * first letter of the page.

View File

@ -38,6 +38,11 @@ class DocumentationPage extends ViewableData
protected $read = false; protected $read = false;
/**
* @var string
*/
protected $canonicalUrl;
/** /**
* @param DocumentationEntity $entity * @param DocumentationEntity $entity
* @param string $filename * @param string $filename
@ -260,10 +265,25 @@ class DocumentationPage extends ViewableData
Controller::join_links( Controller::join_links(
$this->entity->Link($short), $this->entity->Link($short),
$this->getRelativeLink() $this->getRelativeLink()
), '/' ),
'/'
); );
} }
/**
* Determine and set the canonical URL for the given record, for example: dev/docs/en/Path/To/Document
*/
public function populateCanonicalUrl()
{
$url = Director::absoluteURL(Controller::join_links(
Config::inst()->get('DocumentationViewer', 'link_base'),
$this->getEntity()->getLanguage(),
$this->getRelativeLink()
));
$this->setCanonicalUrl($url);
}
/** /**
* Return metadata from the first html block in the page, then remove the * Return metadata from the first html block in the page, then remove the
* block on request * block on request
@ -349,5 +369,30 @@ class DocumentationPage extends ViewableData
{ {
return sprintf(get_class($this) .': %s)', $this->getPath()); return sprintf(get_class($this) .': %s)', $this->getPath());
} }
}
/**
* Set the canonical URL to use for this page
*
* @param string $canonicalUrl
* @return $this
*/
public function setCanonicalUrl($canonicalUrl)
{
$this->canonicalUrl = $canonicalUrl;
return $this;
}
/**
* Get the canonical URL to use for this page. Will trigger discovery
* via {@link DocumentationPage::populateCanonicalUrl()} if none is already set.
*
* @return string
*/
public function getCanonicalUrl()
{
if (!$this->canonicalUrl) {
$this->populateCanonicalUrl();
}
return $this->canonicalUrl;
}
}

View File

@ -2,6 +2,9 @@
<% base_tag %> <% base_tag %>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<% if $CanonicalUrl %>
<link rel="canonical" href="$CanonicalUrl" />
<% end_if %>
<title><% if Title %>$Title &#8211; <% end_if %>$DocumentationTitle</title> <title><% if Title %>$Title &#8211; <% end_if %>$DocumentationTitle</title>
</head> </head>

View File

@ -19,10 +19,8 @@ class DocumentationPageTest extends SapphireTest
Config::nest(); Config::nest();
// explicitly use dev/docs. Custom paths should be tested separately // explicitly use dev/docs. Custom paths should be tested separately
Config::inst()->update( Config::inst()->update('DocumentationViewer', 'link_base', 'dev/docs/');
'DocumentationViewer', 'link_base', 'dev/docs/'
);
$manifest = new DocumentationManifest(true); $manifest = new DocumentationManifest(true);
} }
@ -41,7 +39,7 @@ class DocumentationPageTest extends SapphireTest
'test.md', 'test.md',
DOCSVIEWER_PATH . '/tests/docs/en/test.md' DOCSVIEWER_PATH . '/tests/docs/en/test.md'
); );
// single layer // single layer
$this->assertEquals( $this->assertEquals(
'dev/docs/en/doctest/2.4/test/', $page->Link(), 'dev/docs/en/doctest/2.4/test/', $page->Link(),
@ -53,18 +51,18 @@ class DocumentationPageTest extends SapphireTest
'sort', 'sort',
DOCSVIEWER_PATH . '/tests/docs/en/sort/' DOCSVIEWER_PATH . '/tests/docs/en/sort/'
); );
$this->assertEquals('dev/docs/en/doctest/2.4/sort/', $page->Link()); $this->assertEquals('dev/docs/en/doctest/2.4/sort/', $page->Link());
$page = new DocumentationFolder( $page = new DocumentationFolder(
$this->entity, $this->entity,
'1-basic.md', '1-basic.md',
DOCSVIEWER_PATH . '/tests/docs/en/sort/1-basic.md' DOCSVIEWER_PATH . '/tests/docs/en/sort/1-basic.md'
); );
$this->assertEquals('dev/docs/en/doctest/2.4/sort/basic/', $page->Link()); $this->assertEquals('dev/docs/en/doctest/2.4/sort/basic/', $page->Link());
} }
public function testGetBreadcrumbTitle() public function testGetBreadcrumbTitle()
{ {
$page = new DocumentationPage( $page = new DocumentationPage(
@ -74,13 +72,13 @@ class DocumentationPageTest extends SapphireTest
); );
$this->assertEquals("Test - Doctest", $page->getBreadcrumbTitle()); $this->assertEquals("Test - Doctest", $page->getBreadcrumbTitle());
$page = new DocumentationFolder( $page = new DocumentationFolder(
$this->entity, $this->entity,
'1-basic.md', '1-basic.md',
DOCSVIEWER_PATH . '/tests/docs/en/sort/1-basic.md' DOCSVIEWER_PATH . '/tests/docs/en/sort/1-basic.md'
); );
$this->assertEquals('Basic - Sort - Doctest', $page->getBreadcrumbTitle()); $this->assertEquals('Basic - Sort - Doctest', $page->getBreadcrumbTitle());
$page = new DocumentationFolder( $page = new DocumentationFolder(
@ -91,4 +89,22 @@ class DocumentationPageTest extends SapphireTest
$this->assertEquals('Sort - Doctest', $page->getBreadcrumbTitle()); $this->assertEquals('Sort - Doctest', $page->getBreadcrumbTitle());
} }
public function testGetCanonicalUrl()
{
$page = new DocumentationPage(
$this->entity,
'file.md',
DOCSVIEWER_PATH . '/tests/docs/en/test/file.md'
);
$this->assertContains(
'dev/docs/en/test/file/',
$page->getCanonicalUrl(),
'Canonical URL is determined, set and returned'
);
$page->setCanonicalUrl('some-other-url');
$this->assertSame('some-other-url', $page->getCanonicalUrl(), 'Canonical URL can be adjusted via public API');
}
} }

View File

@ -11,7 +11,7 @@
class DocumentationViewerTest extends FunctionalTest class DocumentationViewerTest extends FunctionalTest
{ {
protected $autoFollowRedirection = false; protected $autoFollowRedirection = false;
protected $manifest; protected $manifest;
public function setUp() public function setUp()
@ -20,7 +20,7 @@ class DocumentationViewerTest extends FunctionalTest
Config::nest(); Config::nest();
// explicitly use dev/docs. Custom paths should be tested separately // explicitly use dev/docs. Custom paths should be tested separately
Config::inst()->update( Config::inst()->update(
'DocumentationViewer', 'link_base', 'dev/docs/' 'DocumentationViewer', 'link_base', 'dev/docs/'
); );
@ -65,11 +65,11 @@ class DocumentationViewerTest extends FunctionalTest
$this->manifest = new DocumentationManifest(true); $this->manifest = new DocumentationManifest(true);
} }
public function tearDown() public function tearDown()
{ {
parent::tearDown(); parent::tearDown();
Config::unnest(); Config::unnest();
} }
@ -98,47 +98,47 @@ class DocumentationViewerTest extends FunctionalTest
$response = $this->get('dev/docs/en/doc_test/3.0/empty.md'); $response = $this->get('dev/docs/en/doc_test/3.0/empty.md');
$this->assertEquals(301, $response->getStatusCode(), 'Direct markdown links also work. They should redirect to /empty/'); $this->assertEquals(301, $response->getStatusCode(), 'Direct markdown links also work. They should redirect to /empty/');
// 2.4 is the stable release. Not in the URL // 2.4 is the stable release. Not in the URL
$response = $this->get('dev/docs/en/doc_test/2.4'); $response = $this->get('dev/docs/en/doc_test/2.4');
$this->assertEquals($response->getStatusCode(), 200, 'Existing base folder'); $this->assertEquals($response->getStatusCode(), 200, 'Existing base folder');
$this->assertContains('english test', $response->getBody(), 'Toplevel content page'); $this->assertContains('english test', $response->getBody(), 'Toplevel content page');
// accessing base redirects to the version with the version number. // accessing base redirects to the version with the version number.
$response = $this->get('dev/docs/en/doc_test/'); $response = $this->get('dev/docs/en/doc_test/');
$this->assertEquals($response->getStatusCode(), 301, 'Existing base folder redirects to with version'); $this->assertEquals($response->getStatusCode(), 301, 'Existing base folder redirects to with version');
$response = $this->get('dev/docs/en/doc_test/3.0/'); $response = $this->get('dev/docs/en/doc_test/3.0/');
$this->assertEquals($response->getStatusCode(), 200, 'Existing base folder'); $this->assertEquals($response->getStatusCode(), 200, 'Existing base folder');
$response = $this->get('dev/docs/en/doc_test/2.3/nonexistant-subfolder'); $response = $this->get('dev/docs/en/doc_test/2.3/nonexistant-subfolder');
$this->assertEquals($response->getStatusCode(), 404, 'Nonexistant subfolder'); $this->assertEquals($response->getStatusCode(), 404, 'Nonexistant subfolder');
$response = $this->get('dev/docs/en/doc_test/2.3/nonexistant-file.txt'); $response = $this->get('dev/docs/en/doc_test/2.3/nonexistant-file.txt');
$this->assertEquals($response->getStatusCode(), 301, 'Nonexistant file'); $this->assertEquals($response->getStatusCode(), 301, 'Nonexistant file');
$response = $this->get('dev/docs/en/doc_test/2.3/nonexistant-file/'); $response = $this->get('dev/docs/en/doc_test/2.3/nonexistant-file/');
$this->assertEquals($response->getStatusCode(), 404, 'Nonexistant file'); $this->assertEquals($response->getStatusCode(), 404, 'Nonexistant file');
$response = $this->get('dev/docs/en/doc_test/2.3/test'); $response = $this->get('dev/docs/en/doc_test/2.3/test');
$this->assertEquals($response->getStatusCode(), 200, 'Existing file'); $this->assertEquals($response->getStatusCode(), 200, 'Existing file');
$response = $this->get('dev/docs/en/doc_test/3.0/empty?foo'); $response = $this->get('dev/docs/en/doc_test/3.0/empty?foo');
$this->assertEquals(200, $response->getStatusCode(), 'Existing page'); $this->assertEquals(200, $response->getStatusCode(), 'Existing page');
$response = $this->get('dev/docs/en/doc_test/3.0/empty/'); $response = $this->get('dev/docs/en/doc_test/3.0/empty/');
$this->assertEquals($response->getStatusCode(), 200, 'Existing page'); $this->assertEquals($response->getStatusCode(), 200, 'Existing page');
$response = $this->get('dev/docs/en/doc_test/3.0/test'); $response = $this->get('dev/docs/en/doc_test/3.0/test');
$this->assertEquals($response->getStatusCode(), 404, 'Missing page'); $this->assertEquals($response->getStatusCode(), 404, 'Missing page');
$response = $this->get('dev/docs/en/doc_test/3.0/test.md'); $response = $this->get('dev/docs/en/doc_test/3.0/test.md');
$this->assertEquals($response->getStatusCode(), 301, 'Missing page'); $this->assertEquals($response->getStatusCode(), 301, 'Missing page');
$response = $this->get('dev/docs/en/doc_test/3.0/test/'); $response = $this->get('dev/docs/en/doc_test/3.0/test/');
$this->assertEquals($response->getStatusCode(), 404, 'Missing page'); $this->assertEquals($response->getStatusCode(), 404, 'Missing page');
$response = $this->get('dev/docs/dk/'); $response = $this->get('dev/docs/dk/');
$this->assertEquals($response->getStatusCode(), 404, 'Access a language that doesn\'t exist'); $this->assertEquals($response->getStatusCode(), 404, 'Access a language that doesn\'t exist');
} }
@ -188,7 +188,7 @@ class DocumentationViewerTest extends FunctionalTest
$response = $v->handleRequest(new SS_HTTPRequest('GET', 'en/doc_test/2.3/subfolder/subsubfolder/subsubpage/'), DataModel::inst()); $response = $v->handleRequest(new SS_HTTPRequest('GET', 'en/doc_test/2.3/subfolder/subsubfolder/subsubpage/'), DataModel::inst());
$this->assertEquals('en', $v->getLanguage()); $this->assertEquals('en', $v->getLanguage());
} }
public function testAccessingAll() public function testAccessingAll()
{ {
@ -230,4 +230,20 @@ class DocumentationViewerTest extends FunctionalTest
// redirect should have been to the absolute url minus the .md extension // redirect should have been to the absolute url minus the .md extension
$this->assertEquals(Director::absoluteURL('dev/docs/en/doc_test/3.0/tutorials/'), $response->getHeader('Location')); $this->assertEquals(Director::absoluteURL('dev/docs/en/doc_test/3.0/tutorials/'), $response->getHeader('Location'));
} }
public function testCanonicalUrlIsIncludedInLayout()
{
$response = $this->get('dev/docs/en/doc_test/2.3/subfolder/subsubfolder/subsubpage');
$this->assertEquals(200, $response->getStatusCode());
$expectedUrl = Director::absoluteURL('dev/docs/en/subfolder/subsubfolder/subsubpage/');
$this->assertContains('<link rel="canonical" href="' . $expectedUrl . '" />', (string) $response->getBody());
}
public function testCanonicalUrlIsEmptyWhenNoPageExists()
{
$viewer = new DocumentationViewer;
$this->assertSame('', $viewer->getCanonicalUrl());
}
} }